├── docs ├── images │ ├── IAM-1.png │ ├── IAM-2.png │ ├── DynamoDB-1.png │ ├── DynamoDB-2.png │ ├── DynamoDB-3.png │ ├── API-Gateway-1.png │ ├── API-Gateway-2.png │ ├── API-Gateway-3.png │ └── Lambda-Create-1.png ├── Lambda_GET.md ├── DynamoDB.md ├── Lambda_POST.md ├── CloudWatch.md ├── Lambda.md ├── IAM.md ├── Lambda_Subscription.md ├── API_Gateway.md └── Whats_Next.md ├── src ├── twitch-webhook-get │ ├── index.js │ └── twitch-webhook-get.zip ├── twitch-webhook-post │ ├── twitch-webhook-post.zip │ ├── package.json │ ├── package-lock.json │ └── index.js └── twitch-webhook-create │ ├── twitch-webhook-create.zip │ ├── package.json │ ├── index.js │ └── package-lock.json ├── LICENSE ├── .gitignore └── README.md /docs/images/IAM-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/docs/images/IAM-1.png -------------------------------------------------------------------------------- /docs/images/IAM-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/docs/images/IAM-2.png -------------------------------------------------------------------------------- /docs/images/DynamoDB-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/docs/images/DynamoDB-1.png -------------------------------------------------------------------------------- /docs/images/DynamoDB-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/docs/images/DynamoDB-2.png -------------------------------------------------------------------------------- /docs/images/DynamoDB-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/docs/images/DynamoDB-3.png -------------------------------------------------------------------------------- /docs/images/API-Gateway-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/docs/images/API-Gateway-1.png -------------------------------------------------------------------------------- /docs/images/API-Gateway-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/docs/images/API-Gateway-2.png -------------------------------------------------------------------------------- /docs/images/API-Gateway-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/docs/images/API-Gateway-3.png -------------------------------------------------------------------------------- /docs/images/Lambda-Create-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/docs/images/Lambda-Create-1.png -------------------------------------------------------------------------------- /src/twitch-webhook-get/index.js: -------------------------------------------------------------------------------- 1 | exports.handler = event => { 2 | return { statusCode: 200, body: event.queryStringParameters['hub.challenge'] }; 3 | }; -------------------------------------------------------------------------------- /src/twitch-webhook-get/twitch-webhook-get.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/src/twitch-webhook-get/twitch-webhook-get.zip -------------------------------------------------------------------------------- /src/twitch-webhook-post/twitch-webhook-post.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/src/twitch-webhook-post/twitch-webhook-post.zip -------------------------------------------------------------------------------- /src/twitch-webhook-create/twitch-webhook-create.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedist/Twitch-Webhook-AWS-Tutorial/HEAD/src/twitch-webhook-create/twitch-webhook-create.zip -------------------------------------------------------------------------------- /src/twitch-webhook-post/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitch-webhook-post", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Jeff Martin ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "crypto": "^1.0.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/twitch-webhook-post/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitch-webhook-post", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "crypto": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", 10 | "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/twitch-webhook-create/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitch-webhook-create", 3 | "version": "1.0.0", 4 | "description": "Tutorial Lambda function to create subscriptions to Twitch Webhooks", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Jeff Martin ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "request": "^2.88.0", 13 | "request-promise": "^4.2.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/Lambda_GET.md: -------------------------------------------------------------------------------- 1 | # GET Handler 2 | This Lambda function is by far the simplest. When we start the subscription process, Twitch sends a GET request to our callback URL to ensure that it is accessible and to confirm this the incoming request contains a 'hub.challenge' header which we simply have to reply to the request with, along with a '200' statusCode. 3 | 4 | ## Step 1: Create a Lambda function 5 | Same process as before, except this time we'll just call it 'twitch-webhook-get', and we'll not need to set any env variables. 6 | 7 | ## Step 2: Code 8 | The code is so simple I'll just paste it here, but it's also available in the '/src/twitch-webhook-get' directory, and as a zip to upload, but copy/paste should suffice. 9 | 10 | ```javascript 11 | exports.handler = event => { 12 | return { statusCode: 200, body: event.queryStringParameters['hub.challenge'] }; 13 | }; 14 | ``` 15 | 16 | ## Step 4: Next 17 | We now have completed the functions needed to create a webhook subscription. The last function we will need to create is the [POST Handler](/docs/Lambda_POST.md) which will handle the incoming notifications. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jeff Martin 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /src/twitch-webhook-post/index.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const AWS = require('aws-sdk'); 3 | const documentClient = new AWS.DynamoDB.DocumentClient({ region: 'eu-west-1' }); 4 | 5 | exports.handler = async event => { 6 | const verify = () => { 7 | const expected = event.headers['x-hub-signature']; 8 | const calculated = 'sha256=' + crypto.createHmac('sha256', process.env.webhook_secret).update(Buffer(event.body)).digest('hex'); 9 | return expected === calculated; 10 | }; 11 | 12 | // If verification fails we log the failure and return a response to the request 13 | if (!verify()) { 14 | console.warn('Verification failed', event); 15 | return { statusCode: 200 }; 16 | } 17 | 18 | let notification = {}; 19 | try { 20 | notification = JSON.parse(event.body); 21 | } 22 | catch(err) { 23 | console.warn(err); 24 | return { statusCode: 200 }; 25 | } 26 | 27 | // This is data we will be adding to your DynamoDB table 28 | const params = { 29 | TableName: 'Twitch-Webhooks', 30 | Item: { 31 | channel: event.queryStringParameters.channel, 32 | timestamp: event.headers['twitch-notification-timestamp'], 33 | type: event.queryStringParameters.type, 34 | id: event.headers['twitch-notification-id'], 35 | data: notification.data[0] || false 36 | }, 37 | }; 38 | 39 | return documentClient.put(params).promise() 40 | .then(() => ({ statusCode: 200 })) 41 | .catch(err => { 42 | console.warn(err); 43 | return { statusCode: 200 }; 44 | }); 45 | }; -------------------------------------------------------------------------------- /docs/DynamoDB.md: -------------------------------------------------------------------------------- 1 | # Creating a DynamodDB table 2 | DynamoDB is a NoSQL database that you pay for only what you use, and the free tier provided by Amazon (that does not expire) allows you enough for 25GB/month storage, and 25 read and 25 write capacity units, which will be more than enough to get started. 3 | 4 | 5 | ## Step 1: DynamoDB Dashboard 6 | Once you're logged in to your AWS dashboard, navigate through the services page to [DynamoDB](https://console.aws.amazon.com/dynamodb/home) which should take you to the DynamoDB dashboard and set to your region, which for me is eu-west-1. From here, just click **Create Table**. 7 | 8 | 9 | ## Step 2: Create a Table 10 | For this example, we'll just call the table *Twitch-Webhooks*, and for the primary key we'll be using *channel* as a partition key and *timestamp* as a sort key. Channel will be the channel ID of that a webhook notification is for, and the timestamp will be the Twitch provided notification timestamp. 11 | 12 | ![Create Table](/docs/images/DynamoDB-1.png) 13 | 14 | ## Step 3: Table Settings 15 | For learning and testing purposes, we don't want this table to scale, especially beyond the free tier limit, and because even the lowest level of 1 read capacity unit and 1 write capacity unit provides 1 read and 1 write of 1KB each per second, that will be sufficient for testing. Once that is set, you can proceed and create the table! 16 | 17 | ![Table Settings](/docs/images/DynamoDB-2.png) 18 | 19 | 20 | ## Step 4: Amazon Resource Name (ARN) 21 | The last thing we need to do here is to make a note of the Amazon Resource Name (ARN) of the table, this is located at the bottom of the overview tab for the table. This ARN will be used in the next step to create a role and give it the rights that will be required to read and write to the table. 22 | 23 | ![ARN](/docs/images/DynamoDB-3.png) 24 | 25 | 26 | ## Next: Creating an IAM role 27 | Now that we have a table ready to store the notifications, and the ARN for it copied, we can now set up a role that will be used by the Lambda functions we will be creating and give it access to this table. [Creating an IAM Role](/docs/IAM.md) -------------------------------------------------------------------------------- /docs/Lambda_POST.md: -------------------------------------------------------------------------------- 1 | # POST Handler 2 | The function to handle POST requests has 2 main parts to it, first it has to verify the authenticity of the notification by the use of the 'crypto' module the secret that was provided during webhook subscription, this allows you to be sure both the integrity of the notification as well as that it was sent by Twitch. Secondly, assuming verification check passed this function then has to put the item into our DynamoDB table. 3 | 4 | ## Step 1: Create a Lambda function 5 | Same process as before, except this time we'll just call it 'twitch-webhook-post', and we will need to set the 'webhook_secret' variable to the same that was set in the 'twitch-webhook-create' function. 6 | 7 | ## Step 2: Code 8 | The code for this function can be found in the '/src/twitch-webhook-post', and much like the first function we created this one needs an additional module, 'crypto' in this case. So the easiest way to create this function in Lambda is to upload the zip. The function also makes use of the AWS SDK module, but this doesn't need to be included as all Node.js Lambda functions can require that module natively. 9 | 10 | The first step of the code is a simple verification step where we take the body of the request sent by Twitch, create a hash of it using our 'webhook_secret', and then compare that to the hash that is included in the request as the 'x-hub-signature' header. 11 | 12 | Next we create the parameters for our DynamoDB 'put' operation. This structure for the item is very basic and uses the channel ID and topic type from the querystring of the request, the notification ID and timestamp values created by Twitch that are in the header of the request, and finally a data field that will contain the notification object. This Item structure should work well for all webhooks topics, but in production it would be beneficial to create separate items for different topics so that redundant data (such as the 'to_id' field in follows, as you already know who the follow is to). 13 | 14 | ## Step 4: Next 15 | We've created all of our Lambda functions, next we need to set up our [API Gateway](/docs/API_Gateway.md) which will proxy requests to these Lambda functions. 16 | 17 | -------------------------------------------------------------------------------- /docs/CloudWatch.md: -------------------------------------------------------------------------------- 1 | # CloudWatch Events 2 | CloudWatch has many great uses, including Lambda function logs, graphs and stats on various AWS services we are running as well as the ability to set up alerts if they go outside of a specified range, and also scheduled tasks. It is the scheduled tasks that we will use to trigger our 'twitch-webhook-create' Lambda function at regular intervals to keep our webhook subscriptions running continuously. 3 | 4 | 5 | ## Step 1: Creating a Rule 6 | From the CloudWatch page, click 'Events' and then 'Create Rule'. From here we have a vast range of event sources that we can use, but for our needs we can just set a schedule as it will always be at a set interval that it will run our Lambda function. The tutorial sample code has a 10 hour lease, and is set to renew if the function is run with less than 2 hours remaining so we can simply set the schedule for every 1 hour and that will work, although in production if using a max lease duration of 10 days then a 1 day interval would work (with a large number of webhook subscriptions it can be advantageous to use these intervals to spread out resubscriptions, rather than let them all be done at once and potentially hit rate-limit bottlenecks). 7 | 8 | For the target we will set 'twitch-webhook-create', and leave the rest of the settings as their defaults. In production we would likely set the alias and use a production version of the Lambda function, but just like stages in the API Gateway, aliases are not essential for this tutorial. 9 | 10 | 11 | ## Step 2: Configure Rule Details 12 | All that remains is to give the rule a name, description, and to leave the state set to enabled. Once we click create rule it will now at the set interval call our Lambda function, which will in turn check our subscription and renew if the lease duration is low. 13 | 14 | 15 | ## Next: End of the Tutorial! 16 | And with this automatic renewal all set up, we now have a serverless webhook system that automatically renews! There were some topics that I touched on briefly during this tutorial that I'll talk a little about, as well as some potential ideas of where to go forward from here now that you have a webhook, in the [What's Next?](/docs/Whats_Next.md) section. 17 | -------------------------------------------------------------------------------- /src/twitch-webhook-create/index.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise'); 2 | 3 | exports.handler = async event => { 4 | const getAppToken = () => { 5 | const options = { 6 | url: `https://id.twitch.tv/oauth2/token?client_id=${process.env.client_id}&client_secret=${process.env.client_secret}&grant_type=client_credentials`, 7 | method: 'POST', 8 | json: true, 9 | }; 10 | 11 | return request(options); 12 | }; 13 | 14 | // The single webhook we're subscribing to in this tutorial 15 | const webhook = { 16 | channel: '32168215', 17 | type: 'follows', 18 | topic: 'https://api.twitch.tv/helix/users/follows?first=1&to_id=32168215' 19 | }; 20 | 21 | // Twitch request to return current subscriptions associated with our app token 22 | let appToken = null; 23 | const checkSubscriptions = res => { 24 | appToken = 'Bearer ' + res.access_token; 25 | 26 | const options = { 27 | url: 'https://api.twitch.tv/helix/webhooks/subscriptions', 28 | headers: { 29 | Authorization: appToken, 30 | 'Client-Id': process.env.client_id 31 | }, 32 | json: true 33 | }; 34 | 35 | return request(options); 36 | }; 37 | 38 | const createSubscription = res => { 39 | // Check if subscription both exists and has at least 2 hours remaining on the lease 40 | const sub = res.data.find(item => item.topic === webhook.topic); 41 | if (sub && (new Date(sub.expires_at) - new Date()) / 3600000 > 2) return; 42 | 43 | const options = { 44 | url: 'https://api.twitch.tv/helix/webhooks/hub', 45 | method: 'POST', 46 | headers: { 47 | Authorization: appToken, 48 | 'Content-Type': 'application/json' 49 | }, 50 | body: JSON.stringify({ 51 | 'hub.callback': `${process.env.domain}?channel=${webhook.channel}&type=${webhook.type}`, 52 | 'hub.mode': 'subscribe', 53 | 'hub.topic': webhook.topic, 54 | 'hub.lease_seconds': 86400, 55 | 'hub.secret': process.env.webhook_secret 56 | }) 57 | }; 58 | 59 | return request(options); 60 | }; 61 | 62 | return getAppToken() 63 | .then(checkSubscriptions) 64 | .then(createSubscription) 65 | .then(res => ({ statusCode: 200 })) 66 | .catch(err => { 67 | console.warn(err); 68 | return { statusCode: 500 }; 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /docs/Lambda.md: -------------------------------------------------------------------------------- 1 | # Lambda functions 2 | Lambda is an amazon service that allows serverless running of code and supports a variety of languages (of which we'll be using Node.js 8.10 in this tutorial). The service is designed so that you're only charge while your function is actually running, so Lambda is ideal for small functions requiring minimal resources that will start, do a thing, then stop in a short space of time. 3 | 4 | Lambda functions are charged in 100ms increments, and are priced per the amount of memory you assign the function (and while not clearly mentioned, increasing memory allowance of a function also increases compute power, so a high CPU low memory function can still benefit from a higher memory setting to reduce its runtime). The smallest is 128MB memory, which charges (at the time of writing this) $0.000000208 per 100ms runtime. However, there is also an indefinite free tier for Lambda, allowing for 400,000 GB-seconds, and 1M free requests each month, which is equivalent of 3.2 million seconds of runtime for a 128MB Lambda function, which far exceeds our needs for this tutorial and not something we will reach. 5 | 6 | 7 | ## [Webhook Subscription](/docs/Lambda_Subscription.md) 8 | The first function we'll be creating is one that will be making a request to Twitch to subscription to a webhook topic. The function will also be what we use to renew subscriptions that are coming close to the end of their lease, so what we'll do is also include a request to check the status of our webhooks, and if they're either missing (which would be the case of the first launch, or if there is some issue that has caused the subscription to be dropped), or close to expiration the subscription request will take place. 9 | 10 | 11 | ## [GET Handler](/docs/Lambda_GET.md) 12 | As part of the subscription process Twitch sends a GET request to our callback URL along with a challenge string, which we will need to reply back to Twitch with along with a 200 status code to complete the webhook subscription process. 13 | 14 | 15 | ## [POST Handler](/docs/Lambda_POST.md) 16 | When a webhook subscription is live, Twitch will send us notifications for that topic as a POST to our callback URL. This Lambda function will handle receiving those notifications, verify their authenticity by using the secret we used when subscribing, and then storing them in the DynamoDB table we previously created. 17 | -------------------------------------------------------------------------------- /docs/IAM.md: -------------------------------------------------------------------------------- 1 | # Creating an IAM role 2 | Identity and Access Management (IAM) is where we'll create a role that gives permission for our Lambda functions to access our previously created DynamoDB table. Going into best practises for managing security is beyond the scope of this tutorial, but I highly encourage reading Amazons own own guides and documentation on the IAM page, especially if you intend to expand any service into a production environment. 3 | 4 | 5 | ## Step 1: IAM Dashboard 6 | Navigate through the services page to [IAM](https://console.aws.amazon.com/iam/home) and from here just click **Roles** from the menu on the left. 7 | 8 | 9 | ## Step 2: Create a Role 10 | From the roles page, click **Create Role** and select Lambda from the list of services that will use this role, then proceed to the Permissions page. 11 | 12 | 13 | ## Step 3: Permissions 14 | In the search box type *lambda* and select **AWSLambdaBasicExecutionRole**. 15 | 16 | ![Permissions](/docs/images/IAM-1.png) 17 | 18 | This role will let Lambda functions log to CloudWatch, which can be immensely useful in monitoring the performance of the functions, as well as providing a way to track potential issues and console output. Next proceed to Review, and we'll name this role 'twitch-webhooks' and finish creating the role. 19 | 20 | 21 | ## Step 4: DynamoDB Policy 22 | Now that a role has been created we can now add a policy to it allowing it to access our DynamoDB table. In the list of roles, click our newly created 'twitch-webhooks' role, and then click *Add inline policy* on the right. 23 | 24 | Select DynamoDB as the service, and from the actions section we want to select the following access levels: 25 | * Read: BatchGetItem, GetItem, Query, Scan. 26 | * Write: BatchWriteItem, DeleteItem, PutItem, UpdateItem. 27 | 28 | Next click in the *resources* section so we can add our tables ARN. Simply click *Add ARN* in the **Table** section, and paste the ARN we got from the DynamoDB page earlier into this and it will auto-fill the boxes. We can leave the Index ARN empty as we wont be querying by index. 29 | 30 | ![ARN](/docs/images/IAM-2.png) 31 | 32 | 33 | ## Step 5: Review 34 | Proceed to reviewing the policy we're adding to this role and give it a name. For a name in this tutorial I choose 'DynamoDB-Twitch-Webhooks-read-write', as at a glance I can see what service this policy is for, the scope (in this case, our Twitch-Webhooks table), and the access it provides. 35 | 36 | 37 | ## Next: Creating Lambda functions 38 | Now we have a DynamoDB table to store our notifications from Twitch, and a role allowing us to access it, next up is creating the Lambda functions that will do all the work of subscribing to and managing our webhooks! [**Creating Lambda functions**](/docs/Lambda.md) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitch-Webhook-AWS-Tutorial 2 | Tutorial for using AWS to create a scalable, serverless, infrastructure to subscribe to, store, and maintain Twitch Webhooks 3 | 4 | 5 | ## About 6 | Twitch Webhooks provide a great way for developers to receive updates on changes to specific API endpoints without needing to constantly poll the endpoint. This tutorial will hopefully explain how to take advantage of serverless technologies to make use of webhooks but without the need to maintain a constantly on webserver. 7 | 8 | Please also keep in mind that this tutorial is for illustrative purposes only, and if you intend to take these services forward into a production environment then please ensure access rights, database design, any auto-scaling, and all relevant security precautions are configured correctly for your use case. 9 | 10 | For this tutorial I'll be using Amazon Web Services (AWS) for the backend: 11 | * DynamoDB - Database to store incoming notifications from Twitch. 12 | * IAM - Access rights management between services. 13 | * Lambda - Run Node.js apps to handle webhook subscription and incoming notifications. 14 | * API Gateway - Web accessible endpoint for Twitch's GET/POST requests. 15 | * CloudWatch - Monitoring of AWS services and logging of Lambda output. 16 | 17 | 18 | ## Getting Started 19 | This guide assumes you already have an AWS account, if not please sign up at https://aws.amazon.com/ Because of the way different services rely on each other, I'll be starting the tutorial with creating a DynamoDB table so that we'll have the ARN resource id that will be needed to create an IAM role. This role will then be used for Lambda function execution, and then finally these functions will be called by the API gateway routes. 20 | 21 | 22 | ## Documentation 23 | 1. [**Creating a DynamodDB Table**](/docs/DynamoDB.md) 24 | 1. [**Creating an IAM role**](/docs/IAM.md) 25 | 1. [**Lambda functions**](/docs/Lambda.md) 26 | 1. [Webhook Subscription](/docs/Lambda_Subscription.md) 27 | 1. [GET Handler](/docs/Lambda_GET.md) 28 | 1. [POST Handler](/docs/Lambda_POST.md) 29 | 1. [**API Gateway Routes**](/docs/API_Gateway.md) 30 | 1. [**What's Next?**](/docs/Whats_Next.md) 31 | 32 | 33 | ## Author 34 | * **Jeff Martin** - [theDist](https://twitch.tv/thedist) 35 | 36 | If you need to get in touch either message me here on GitHub, on Twitch's [Dev forums](https://discuss.dev.twitch.tv), or on the TwitchDev server in the Twitch app. If you liked this tutorial I'd appreciate anyone who wants to follow me on Twitch using my channel link above. I plan to write more tutorials on different subjects as I try new things, some of which I plan to stream in the future too! 37 | 38 | 39 | ## License 40 | 41 | This project is licensed under the MIT License - see the LICENSE file for details 42 | -------------------------------------------------------------------------------- /docs/Lambda_Subscription.md: -------------------------------------------------------------------------------- 1 | # Webhook Subscription 2 | For this Lambda function, we will be going through the process to create the function in Lambda, and then use Node.js to first request our current webhook subscriptions from Twitch and then if a subscription is not present or close to lease expiration we will request a new subscription. 3 | 4 | It is worth noting that in this tutorial I am generating an app access token as part of the function as the check webhook subscription endpoints requires an app access token. In a production environment I have separate services that manage creating and renewing my various OAuth tokens and I just have my production functions get a token from that rather than having to make a request to Twitch. If you this is the only place you're using an app access token then you can use something like what the tutorial function does, otherwise consider a solution that meets your own use case and needs. 5 | 6 | 7 | ## Step 1: Creating a Lambda Function 8 | Navigate to the [Lambda](https://console.aws.amazon.com/lambda/home) dashboard and from here just click the **Create Function** button. We will be authoring an app from scratch so leave that selected, for the name we'll call this one 'twitch-webhook-create', change the runtime to Node.js 8.10 and as we've already created an IAM role for this we can choose the existing role 'twitch-webhooks'. 9 | 10 | 11 | ## Step 2: Setting environment variables 12 | For this tutorial we'll be using 'client_id', 'client_secret', and 'webhook_secret' env variable in our requests, so before we start coding scroll down to the environment variables section and enter your client id and secret that can be found on your Twitch Developer dashboard, and the webhook secret is whatever you want to use and will be used for notification verification later. 13 | 14 | ![ENV Var](/docs/images/Lambda-Create-1.png) 15 | 16 | 17 | ## Step 3: Code 18 | For functions that don't require modules, or for when you're making changes to a Lambda function that already contains required modules, the Lambda code editor is sufficient, but the easiest way to develop Lambda functions is to develop them locally and using NPM to download any modules you may need and then simply zip the files and use Lambda's ability to upload a zip file. 19 | 20 | There's a large number of modules to perform HTTP requests, each with their own pros and cons, for this tutorial I'll be using 'request-promise' as a large portion of Node.js developers are familiar with request, and this just bundles it with Bluebird promises. 21 | 22 | The code for the tutorial can be found in the '/src/twitch-webhook-create/' directory of this project, along with the zip you can upload to Lambda that contains all of the code and required modules for you. 23 | 24 | You can see from the code, there are 4 basic steps in this Lambda function. 25 | 1. Get app access token. 26 | 1. Get list of current subscriptions. 27 | 1. Create new subscription if needed. 28 | 1. return a '200' status code on success, or '500' on failure 29 | 30 | The callback we use for the webhook can be done in multiple ways, you can either use a simple URL but then you have to look through the headers on notifications to figure out what topic a notification is for. 31 | 32 | Another way is to use use the channel and type of a webhook topic in the URL path, which works very well for Express apps, and can be done through API Gateway easily. 33 | 34 | For simplicity though I've used querystring parameters so that when a notification comes in Lambda will take care of parsing the parameters and we can easily access them through the event object. 35 | 36 | 37 | ## Step 4: Next 38 | The function is almost complete, but not quite ready for testing. The reason for this is that we can't set the callback URL in the env variables until we've configured API Gateway, and even then we have yet to set up the other functions required for this all to work so next is to create the [GET Handler](/docs/Lambda_GET.md). -------------------------------------------------------------------------------- /docs/API_Gateway.md: -------------------------------------------------------------------------------- 1 | # API Gateway 2 | API Gateway is a service that will be our internet facing part of this tutorial, it provides the routes that Twitch will use to send GET and POST requests to which will then be passed on to our Lambda functions. This is one of the services that we'll be using that has only a limited 12 month trial of 1 million free request, after which it will cost $3.50 per million requests at the time of writing this, plus data transfer costs, but both of this will be almost negligible at the scale that this tutorial is for. 3 | 4 | 5 | ## Step 1: Create our API 6 | From the API Gateway dashboard, click Create API. The creation settings should be self-explanatory, the only one worth mentioning is endpoint type, and this is set to edge optimised because our API will act as a gateway between internal services, such as Lambda, and the internet. If you was creating an API to use between services within the same AWS Region, then Regional API Endpoint would be used, and Private API Endpoint is for AWS VPC, neither of which we will deal with in this tutorial. 7 | 8 | ![Create API](/docs/images/API-Gateway-1.png) 9 | 10 | 11 | ## Step 2: Creating a GET Method 12 | From our resources page of our API we now need to create the methods we will be using. From the actions dropdown, click 'Create Method', this will create a little dropdown box in root of of API which we will set to 'GET' and then click the tick to confirm. 13 | 14 | This method is what controls what happens when a GET request is made to our API, for this tutorial we'll be using a Lambda Function as integration type, with Lambda Proxy Integration toggled which will cause the entire HTTP request to be passed on to the Lambda function. Select whichever Lambda Region you use, the default varies depending on your AWS account settings but for me this is 'eu-west-1'. Finally we set the Lambda function we'd like to call, in this case it is our 'twitch-webhooks-get' function. 15 | 16 | ![Method Settings](/docs/images/API-Gateway-2.png) 17 | 18 | 19 | ## Step 3: Creating a POST Method 20 | To create the POST method for the incoming Twitch notifications we just follow the same process we used to create the GET Method, except we will select 'POST' from the method dropdown, and the function name is 'twitch-webhooks-post'. 21 | 22 | 23 | ## Step 4: Deploy 24 | For the API to be reachable, we now have to deploy it by clicking 'Deploy API' in the actions dropdown menu. Since this is the first time time we're deploying our API we will need to create a stage to deploy to. For the tutorial it will be perfectly fine to use a single stage, but in a production environment you will likely want to use both a development stage and a production stage for reasons that will be mentioned in the closing sections of this tutorial. 25 | 26 | ![Deploy Stage](/docs/images/API-Gateway-3.png) 27 | 28 | 29 | ## Step 5: API URL 30 | From the stages section of our API, you can now see an 'Invoke URL', which should look something like `https://someIdHere.execute-api.eu-west-1.amazonaws.com/DEV`. This is now an internet accessible address for our GET and POST lambda functions. As we now how this URL, go back to the configuration of of our 'twitch-webhooks-create' function in the Lambda dashboard, and add that URL as the 'domain' env variable, this will now let the function use that domain as the callback URL for our webhook subscriptions. 31 | 32 | 33 | ## Step 6: Checking it works 34 | Now we can finally start our webhook subscription! To start it up we can simply click the test button in the top right, and as we're not passing any variables to the script you can just ignore needing to pass anything in our test function, and this test will run our function and cause the subscription process to take place. To test that it works, if you've been using the samples I've provided then the webhook will be one that sends notifications for new followers to the channel id 32168215, which happens to be my channel https://twitch.tv/theDist so to shamelessly plug my own channel go there and follow it. 35 | 36 | If all goes according to plan then you can go to the Items tab of your DynamoDB table page and should see an entry for the follow! If there is an issue at some point though, every Lambda function has its own logs on CloudWatch so by going to the logs section of CloudWatch and selecting each function you can view the function calls and see if any of them are showing errors (and usefully for debugging, any console output you add to the function will be included in these logs so it becomes easy to add console.log() to functions while testing and watching the logs). 37 | 38 | 39 | ## Next: Automatic Renewal 40 | We now have the means to subscribe to a webhook, and the API to handle incoming requests, but a subscription has a limited lease time so we need a way to automate resubscription, and that's where [CloudWatch Events](/docs/CloudWatch.md) comes in. -------------------------------------------------------------------------------- /docs/Whats_Next.md: -------------------------------------------------------------------------------- 1 | # What's Next 2 | There were some points I made during this tutorial about things that could be done in a production environment that I think are worth mentioning here, as well as an example showing how this webhook system we just made can be put to a practical use, and a few helpful ideas of things worth looking to and learning about. 3 | 4 | 5 | ## Lambda Aliases and API Stages 6 | When you're in an environment where you need to make updates and changes to one of your services, but can't afford to have the system go offline or break while you're making this changes, then a great way to go about doing this is to use Lambda Aliases and API Stages. 7 | 8 | By default, a Lambda function that is run by the API Gateway will be whatever is the latest version, which has some risks to it if you're testing out some new things that might break functionality. To deal with this we can create 2 (or more) Aliases, 'DEV', and 'PROD', for development and production versions. This way you can set the DEV alias to always point to whatever is the latest version that you're currently working on, while the PROD alias will be set to a specific version of the function that you know is good. 9 | 10 | In the API Gateway, when we choose what Lambda function a particular method calls, we can add a version to the end of the function name, but rather than setting DEV or PROD, we can just specify `:${stageVariables.lambdaAlias}`, and then whatever we set as the 'lambdaAlias' value in the stage variables setting is what will be called. So when the DEV stage URL is used it can call the DEV lambda function, and whenever the PROD API URL is called it'll start the PROD Lambda function. 11 | 12 | Using this setup means that we can run multiple versions side by side without needing to duplicate API Gateway or Lambda functions, the different Stage URLS just access different functions! 13 | 14 | 15 | ## API Gateway Custom Domain 16 | It's possible to use your own domain name with API Gateway, it's as simple as going through a little config and adding a CNAME DNS record on your domain that points to your API. Then rather than a long amazon URL you can point a 'webhooks' subdomain to the API and use `webhooks.yourdomain.com`, and through path mapping you can point different API's to different paths, such as `webhooks.yourdomain.com/dev` and `webhooks.yourdomain.com/prod` or point to an entirely separate API that you've set up. 17 | 18 | 19 | ## API Gateway Tweaks 20 | There are far too many config options and ways to go about utilising API Gateway to go into detail here, but one nice feature is validating request querystring/headers/body. One example of how this might be used would be on the GET method we set up in this tutorial, where the only use it has is for handling the 'hub.challenge', so if an incoming request lacks that header it would be beneficial to just have API Gateway immediately respond that a header is missing and skip ever having to run the Lambda function. By validating expected requests we can ensure that Lambda time isn't needlessly wasted which will help save costs and reduce needless load on any services that your function might make use of. 21 | 22 | 23 | ## Stream Going Live Example 24 | One use case for a webhook setup like that in this tutorial would be to send a Tweet, or Discord message, when a stream goes live. An easy way to accomplish this is with Lambda and DynamoDB Streams. What DynamoDB Streams can do is act as trigger to start a Lambda function, and pass it some data. So you could create a Lambda function that utilises the Twitter or Discord API to send messages, and any time a webhook notification comes in it will be added to the table, which will then pass that update on through the stream to the Lambda function. 25 | 26 | Because the Stream webhook has recently changed to include notifications of title, game, and other parameter changes rather than just stream up/down, one use case might be to use a single entry in the table per channel and update that entry with new notifications. This has the advantage of the DynamoDB stream can send both the previous state, and the updated state to the function, from there you can determine if the stream went live/offline, or maybe changed game or title and from that determine if the Lambda function should create, delete, or edit the announcement based on that change. 27 | 28 | 29 | ## Downsides 30 | As with all solutions, there are both pros and cons. I'm still exploring various AWS services and trying new things myself so I'm not able to give in depth advice on everything, but one issue I've encountered is the way in which DynamoDB works. It's great as a simple store of a few fields, and supports a variety of field types, but coming from a MongoDB background I feel that the limitations on indexes/primary keys, as well as depth of queries and aggregation just isn't there for DynamoDB. Maybe there are ways to get more out of it that I haven't explored yet, or maybe DynamoDB simply has it's own use cases and outside of that you're expected to use Amazons more expensive database services. 31 | -------------------------------------------------------------------------------- /src/twitch-webhook-create/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitch-webhook-create", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "asn1": { 8 | "version": "0.2.4", 9 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 10 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 11 | "requires": { 12 | "safer-buffer": "~2.1.0" 13 | } 14 | }, 15 | "assert-plus": { 16 | "version": "1.0.0", 17 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 18 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 19 | }, 20 | "asynckit": { 21 | "version": "0.4.0", 22 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 23 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 24 | }, 25 | "aws-sign2": { 26 | "version": "0.7.0", 27 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 28 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 29 | }, 30 | "aws4": { 31 | "version": "1.8.0", 32 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 33 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 34 | }, 35 | "bcrypt-pbkdf": { 36 | "version": "1.0.2", 37 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 38 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 39 | "optional": true, 40 | "requires": { 41 | "tweetnacl": "^0.14.3" 42 | } 43 | }, 44 | "bluebird": { 45 | "version": "3.5.2", 46 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz", 47 | "integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==" 48 | }, 49 | "caseless": { 50 | "version": "0.12.0", 51 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 52 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 53 | }, 54 | "combined-stream": { 55 | "version": "1.0.7", 56 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", 57 | "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", 58 | "requires": { 59 | "delayed-stream": "~1.0.0" 60 | } 61 | }, 62 | "core-util-is": { 63 | "version": "1.0.2", 64 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 65 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 66 | }, 67 | "dashdash": { 68 | "version": "1.14.1", 69 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 70 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 71 | "requires": { 72 | "assert-plus": "^1.0.0" 73 | } 74 | }, 75 | "delayed-stream": { 76 | "version": "1.0.0", 77 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 78 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 79 | }, 80 | "ecc-jsbn": { 81 | "version": "0.1.2", 82 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 83 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 84 | "optional": true, 85 | "requires": { 86 | "jsbn": "~0.1.0", 87 | "safer-buffer": "^2.1.0" 88 | } 89 | }, 90 | "extend": { 91 | "version": "3.0.2", 92 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 93 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 94 | }, 95 | "extsprintf": { 96 | "version": "1.3.0", 97 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 98 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 99 | }, 100 | "fast-json-stable-stringify": { 101 | "version": "2.0.0", 102 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 103 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 104 | }, 105 | "forever-agent": { 106 | "version": "0.6.1", 107 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 108 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 109 | }, 110 | "form-data": { 111 | "version": "2.3.2", 112 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", 113 | "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", 114 | "requires": { 115 | "asynckit": "^0.4.0", 116 | "combined-stream": "1.0.6", 117 | "mime-types": "^2.1.12" 118 | }, 119 | "dependencies": { 120 | "combined-stream": { 121 | "version": "1.0.6", 122 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", 123 | "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", 124 | "requires": { 125 | "delayed-stream": "~1.0.0" 126 | } 127 | } 128 | } 129 | }, 130 | "getpass": { 131 | "version": "0.1.7", 132 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 133 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 134 | "requires": { 135 | "assert-plus": "^1.0.0" 136 | } 137 | }, 138 | "har-schema": { 139 | "version": "2.0.0", 140 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 141 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 142 | }, 143 | "har-validator": { 144 | "version": "5.1.5", 145 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", 146 | "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 147 | "requires": { 148 | "ajv": "^6.12.3", 149 | "har-schema": "^2.0.0" 150 | }, 151 | "dependencies": { 152 | "ajv": { 153 | "version": "6.12.6", 154 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 155 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 156 | "requires": { 157 | "fast-deep-equal": "^3.1.1", 158 | "fast-json-stable-stringify": "^2.0.0", 159 | "json-schema-traverse": "^0.4.1", 160 | "uri-js": "^4.2.2" 161 | } 162 | }, 163 | "fast-deep-equal": { 164 | "version": "3.1.3", 165 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 166 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 167 | }, 168 | "json-schema-traverse": { 169 | "version": "0.4.1", 170 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 171 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 172 | } 173 | } 174 | }, 175 | "http-signature": { 176 | "version": "1.2.0", 177 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 178 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 179 | "requires": { 180 | "assert-plus": "^1.0.0", 181 | "jsprim": "^1.2.2", 182 | "sshpk": "^1.7.0" 183 | } 184 | }, 185 | "is-typedarray": { 186 | "version": "1.0.0", 187 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 188 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 189 | }, 190 | "isstream": { 191 | "version": "0.1.2", 192 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 193 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 194 | }, 195 | "jsbn": { 196 | "version": "0.1.1", 197 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 198 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 199 | "optional": true 200 | }, 201 | "json-stringify-safe": { 202 | "version": "5.0.1", 203 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 204 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 205 | }, 206 | "jsprim": { 207 | "version": "1.4.2", 208 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", 209 | "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 210 | "requires": { 211 | "assert-plus": "1.0.0", 212 | "extsprintf": "1.3.0", 213 | "json-schema": "0.4.0", 214 | "verror": "1.10.0" 215 | }, 216 | "dependencies": { 217 | "json-schema": { 218 | "version": "0.4.0", 219 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 220 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 221 | } 222 | } 223 | }, 224 | "lodash": { 225 | "version": "4.17.21", 226 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 227 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 228 | }, 229 | "mime-db": { 230 | "version": "1.36.0", 231 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", 232 | "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" 233 | }, 234 | "mime-types": { 235 | "version": "2.1.20", 236 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", 237 | "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", 238 | "requires": { 239 | "mime-db": "~1.36.0" 240 | } 241 | }, 242 | "oauth-sign": { 243 | "version": "0.9.0", 244 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 245 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 246 | }, 247 | "performance-now": { 248 | "version": "2.1.0", 249 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 250 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 251 | }, 252 | "psl": { 253 | "version": "1.1.29", 254 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", 255 | "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" 256 | }, 257 | "punycode": { 258 | "version": "1.4.1", 259 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 260 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 261 | }, 262 | "qs": { 263 | "version": "6.5.3", 264 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", 265 | "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" 266 | }, 267 | "request": { 268 | "version": "2.88.0", 269 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 270 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 271 | "requires": { 272 | "aws-sign2": "~0.7.0", 273 | "aws4": "^1.8.0", 274 | "caseless": "~0.12.0", 275 | "combined-stream": "~1.0.6", 276 | "extend": "~3.0.2", 277 | "forever-agent": "~0.6.1", 278 | "form-data": "~2.3.2", 279 | "har-validator": "~5.1.0", 280 | "http-signature": "~1.2.0", 281 | "is-typedarray": "~1.0.0", 282 | "isstream": "~0.1.2", 283 | "json-stringify-safe": "~5.0.1", 284 | "mime-types": "~2.1.19", 285 | "oauth-sign": "~0.9.0", 286 | "performance-now": "^2.1.0", 287 | "qs": "~6.5.2", 288 | "safe-buffer": "^5.1.2", 289 | "tough-cookie": "~2.4.3", 290 | "tunnel-agent": "^0.6.0", 291 | "uuid": "^3.3.2" 292 | } 293 | }, 294 | "request-promise": { 295 | "version": "4.2.2", 296 | "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", 297 | "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", 298 | "requires": { 299 | "bluebird": "^3.5.0", 300 | "request-promise-core": "1.1.1", 301 | "stealthy-require": "^1.1.0", 302 | "tough-cookie": ">=2.3.3" 303 | } 304 | }, 305 | "request-promise-core": { 306 | "version": "1.1.1", 307 | "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", 308 | "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", 309 | "requires": { 310 | "lodash": "^4.13.1" 311 | } 312 | }, 313 | "safe-buffer": { 314 | "version": "5.1.2", 315 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 316 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 317 | }, 318 | "safer-buffer": { 319 | "version": "2.1.2", 320 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 321 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 322 | }, 323 | "sshpk": { 324 | "version": "1.14.2", 325 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", 326 | "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", 327 | "requires": { 328 | "asn1": "~0.2.3", 329 | "assert-plus": "^1.0.0", 330 | "bcrypt-pbkdf": "^1.0.0", 331 | "dashdash": "^1.12.0", 332 | "ecc-jsbn": "~0.1.1", 333 | "getpass": "^0.1.1", 334 | "jsbn": "~0.1.0", 335 | "safer-buffer": "^2.0.2", 336 | "tweetnacl": "~0.14.0" 337 | } 338 | }, 339 | "stealthy-require": { 340 | "version": "1.1.1", 341 | "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", 342 | "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" 343 | }, 344 | "tough-cookie": { 345 | "version": "2.4.3", 346 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 347 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 348 | "requires": { 349 | "psl": "^1.1.24", 350 | "punycode": "^1.4.1" 351 | } 352 | }, 353 | "tunnel-agent": { 354 | "version": "0.6.0", 355 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 356 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 357 | "requires": { 358 | "safe-buffer": "^5.0.1" 359 | } 360 | }, 361 | "tweetnacl": { 362 | "version": "0.14.5", 363 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 364 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 365 | "optional": true 366 | }, 367 | "uri-js": { 368 | "version": "4.4.1", 369 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 370 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 371 | "requires": { 372 | "punycode": "^2.1.0" 373 | }, 374 | "dependencies": { 375 | "punycode": { 376 | "version": "2.1.1", 377 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 378 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 379 | } 380 | } 381 | }, 382 | "uuid": { 383 | "version": "3.3.2", 384 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 385 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 386 | }, 387 | "verror": { 388 | "version": "1.10.0", 389 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 390 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 391 | "requires": { 392 | "assert-plus": "^1.0.0", 393 | "core-util-is": "1.0.2", 394 | "extsprintf": "^1.2.0" 395 | } 396 | } 397 | } 398 | } 399 | --------------------------------------------------------------------------------