├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── README_SAR.md ├── THIRD-PARTY ├── ask-resources.json ├── instructions ├── 1-salesforce-setup.md ├── 2-deploy.md ├── 3-account-linking.md ├── 4-testing.md └── 5-distribute-private-skills.md ├── lambda └── custom │ ├── constants.js │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── resourceStrings.js │ ├── salesforce.js │ ├── secureHandlers.js │ ├── template.yaml │ └── voiceCodeHandlers.js └── skill-package ├── interactionModels └── custom │ └── en-US.json └── skill.json /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.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 | .DS_Store 61 | 62 | # ignore SAM model output 63 | output*.yaml 64 | 65 | # ignore local development 66 | *.vscode 67 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/alexa/skill-sample-nodejs-salesforce/issues), or [recently closed](https://github.com/alexa/skill-sample-nodejs-salesforce/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/alexa/skill-sample-nodejs-salesforce/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/alexa/skill-sample-nodejs-salesforce/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | skill-sample-nodejs-salesforce 2 | Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Private Alexa Skill With Salesforce Integration 2 | 3 | 4 | 5 | [![Salesforce Setup](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-1-off._TTH_.png)](./instructions/1-salesforce-setup.md)[![Deploy](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-2-off._TTH_.png)](./instructions/2-deploy.md)[![Account Linking](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-3-off._TTH_.png)](./instructions/3-account-linking.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-4-off._TTH_.png)](./instructions/4-testing.md)[![Distribute Private Skills](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-5-off._TTH_.png)](./instructions/5-distribute-private-skills.md) 6 | 7 | ## Introduction 8 | 9 | This skill demonstrates how to build a private Alexa skill to access Salesforce data. It includes using account linking, via a connected app in Salesforce, along with a voice code confirmation process that stores the code as a custom setting in Salesforce. Then, you can access opportunities and make updates to them using your voice. 10 | 11 | ## Pre-requisites 12 | 13 | This is a NodeJS Lambda function and skill defintion to be used by [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html). 14 | 15 | You need to initialize ASK CLI with 16 | 17 | ```bash 18 | $ ask init 19 | ``` 20 | 21 | You need an [AWS account](https://aws.amazon.com) and an [Amazon developer account](https://developer.amazon.com) to create an Alexa Skill. 22 | 23 | In order to use the ASK CLI features to automatically deploy and manage your Lambda skill, ensure that you have AWS credentials set up with the appropriate permissions on the computer to which you are installing ASK CLI, as described in [Set Up Credentials for an Amazon Web Services (AWS) Account](https://developer.amazon.com/docs/smapi/set-up-credentials-for-an-amazon-web-services-account.html). 24 | 25 | Clone or download this repository. Then you need to download NodeJS dependencies : 26 | 27 | ```bash 28 | $ (cd lambda/custom && npm install) 29 | ``` 30 | 31 | You need a [Salesforce Trailhead Playground](https://trailhead.salesforce.com/en/modules/trailhead_playground_management/units/create-a-trailhead-playground). 32 | 33 | 34 | ## Objectives 35 | 36 | Together, we'll build a skill that is invoked with the name Salesforce Demo. 37 | 38 | ```text 39 | Alexa, open Salesforce Demo 40 | ``` 41 | 42 | Let's get started! 43 | 44 | 1. **Salesforce Setup** - Set up a Salesforce org using Trailhead Playground. 45 | 2. **Deploy** - Customize and deploy the provided skill. 46 | 3. **Account Linking** - Create a Connected App in Salesforce to use for Account Linking to the Alexa skill. 47 | 4. **Testing** - Make sure everything works. 48 | 5. **Distribute Private Skills** - Learn about Alexa for Business and how to distribute private skills. 49 | 50 | [![Get Started](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/general/buttons/button_get_started._TTH_.png)](./instructions/1-salesforce-setup.md) 51 | 52 | -------------------------------------------------------------------------------- /README_SAR.md: -------------------------------------------------------------------------------- 1 | # Private Alexa Skill With Salesforce Integration for Serverless Application Repository 2 | 3 | ![ASK](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/quiz-game/header._TTH_.png) 4 | 5 | ## Introduction 6 | 7 | This Serverless Application is the back-end of a private Alexa skill that integrates with Salesforce. It includes using account linking, via a connected app in Salesforce, along with a voice code confirmation process that stores the code as a custom setting in Salesforce. Then, you can access opportunities and make updates to them using your voice. 8 | 9 | In order to use the Alexa skill, you will need to complete a few other necessary steps to set up other resources. 10 | 11 | Ultimately, we'll build a skill that is invoked with the name Salesforce Demo. 12 | 13 | ```text 14 | Alexa, open Salesforce Demo 15 | ``` 16 | 17 | Let's get started! 18 | 19 | ## Part 1. Salesforce Setup 20 | 21 | One of the parameters for this application is the Salesforce Instance URL. If you do not have a Salesforce instance, follow the directions below to obtain a [Salesforce Trailhead Playground](https://trailhead.salesforce.com/en/modules/trailhead_playground_management/units/create-a-trailhead-playground). 22 | 23 | ## Part 2. Deploy this Serverless App 24 | 25 | 1. Using the Serverless Application Repository, deploy this application by following the prompts. Fill in your Salesforce instance URL to match your Trailhead Playground site. It will look something like this: ```https://brave-moove-406615-dev-ed.my.salesforce.com```. 26 | 2. Once the deployment was completed, go to your [CloudFormation Stacks](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks) and find the stack you just deployed. 27 | 3. Click on the stack name. 28 | 4. Expand the **Resources** menu. 29 | 5. Copy the Lambda Function Name that was created. It should look something like this: ```aws-serverless-repository-AlexaSalesforceFunction-``` 30 | 31 | ## Part 3. Deploying the Alexa Skill front-end 32 | 33 | Once you've deployed the Serverless App, you need to deploy the Alexa Skill front-end. 34 | 35 | 1. You can use the [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html) to deploy your Alexa skill voice user interface. 36 | 2. Once you have installed the CLI, you need to initialize it with the following command: 37 | 38 | ```bash 39 | $ ask init 40 | ``` 41 | 42 | Note: You need an [Amazon developer account](https://developer.amazon.com) to create an Alexa Skill. 43 | 44 | 3. Clone or download [https://github.com/Alexa/skill-sample-nodejs-salesforce](https://github.com/Alexa/skill-sample-nodejs-salesforce). 45 | 4. Navigate into the ```skill-sample-nodejs-salesforce``` directory. 46 | 5. Set your Lambda function name in the Alexa config file. In the directory you just downloaded, modify the ```.ask/config``` file to have the Lambda ARN that you saved in the previous step. You want to change the ```"uri"``` setting to match that ARN. For example: 47 | 48 | ```json 49 | "apis": { 50 | "custom": { 51 | "endpoint": { 52 | "uri": "-AlexaSalesforceFunction-" 53 | } 54 | } 55 | } 56 | ``` 57 | 58 | 6. Deploy the Alexa skill with the following command (it will also link your Lambda source code in the repository to the skill): 59 | 60 | ```bash 61 | ask deploy 62 | ``` 63 | 64 | 7. Make sure to save your skill ID that comes out from the ```deploy``` command, as you need it later in the instructions. 65 | 66 | ``` 67 | -------------------- Create Skill Project -------------------- 68 | Profile for the deployment: [default] 69 | Skill Id: **** 70 | Skill deployment finished. 71 | Model deployment finished. 72 | Lambda deployment finished. 73 | Your skill is now deployed and enabled in the development stage. 74 | Try invoking the skill by saying “Alexa, open {your_skill_invocation_name}” or simulate an invocation via the `ask simulate` command. 75 | ``` 76 | 77 | 8. Enable Testing - In order to test the skill before publishing, you need to enable testing on the Alexa Developer Console. 78 | 79 | You can directly jump to the page by substituting your Skill ID into the following URL: ```https://developer.amazon.com/alexa/console/ask/test//development/en_US``` 80 | 81 | Click the slider next to Disabled for testing. It should now say Enabled. 82 | 83 | 9. For extra security, you can also go to your Lambda function and re-create the Alexa Skills Kit trigger with Skill ID verification enabled. For more details, see [the Alexa Skills Kit documentation](https://developer.amazon.com/docs/custom-skills/host-a-custom-skill-as-an-aws-lambda-function.html#configuring-the-alexa-skills-kit-trigger). 84 | 85 | ## Continue with Github instructions 86 | 87 | At this point, you can continue with the rest of the setup starting at [Step 3: Account Linking](https://github.com/alexa/skill-sample-nodejs-salesforce/blob/master/instructions/3-account-linking.md) in the Github repository. 88 | 89 | 90 | -------------------------------------------------------------------------------- /THIRD-PARTY: -------------------------------------------------------------------------------- 1 | ** bcryptjs; version 2.4.3 -- https://www.npmjs.com/package/bcryptjs 2 | bcrypt.js 3 | --------- 4 | Copyright (c) 2012 Nevins Bartolomeo 5 | Copyright (c) 2012 Shane Girish 6 | Copyright (c) 2014 Daniel Wirtz 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions 10 | are met: 11 | 1. Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 3. The name of the author may not be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 20 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 21 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 22 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 24 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 28 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | ----- 31 | 32 | ** nforce; version 1.10.0 -- https://www.npmjs.com/package/nforce 33 | Copyright (c) 2015 Kevin M. O'Hara 34 | 35 | Permission is hereby granted, free of charge, to any person 36 | obtaining a copy of this software and associated documentation 37 | files (the "Software"), to deal in the Software without 38 | restriction, including without limitation the rights to use, 39 | copy, modify, merge, publish, distribute, sublicense, and/or 40 | sell copies of the Software, and to permit persons to whom 41 | the Software is furnished to do so, subject to the following 42 | conditions: 43 | 44 | The above copyright notice and this permission notice shall 45 | be included in all copies or substantial portions of the 46 | Software. 47 | 48 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 49 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 50 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 51 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 52 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 53 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 54 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 55 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 56 | -------------------------------------------------------------------------------- /ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skill-package" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "lambda/custom" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "userConfig": { 15 | "runtime": "nodejs10.x", 16 | "handler": "index.handler", 17 | "awsRegion": "us-east-1" 18 | }, 19 | "type": "@ask-cli/lambda-deployer" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /instructions/1-salesforce-setup.md: -------------------------------------------------------------------------------- 1 | # Salesforce Setup 2 | 3 | [![Salesforce Setup](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-1-on._TTH_.png)](./1-salesforce-setup.md)[![Deploy](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-2-off._TTH_.png)](./2-deploy.md)[![Account Linking](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-3-off._TTH_.png)](./3-account-linking.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-4-off._TTH_.png)](./4-testing.md)[![Distribute Private Skills](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-5-off._TTH_.png)](./5-distribute-private-skills.md) 4 | 5 | ## Part 1: Create a Salesforce Trailhead Playground 6 | 7 | In order to build our skill, you need a Salesforce org to interact with. A Salesforce Trailhead Playground is free and allows you to create everything you will need to complete this skill. 8 | 9 | 1. Go to the [Trailhead Playground Management Module](https://trailhead.salesforce.com/modules/trailhead_playground_management) to create a Trailhead Playground (TP) org. 10 | 2. Complete units 1 & 2 in order to set up your TP and obtain your username and login credentials. 11 | 12 | ## Part 2: Create a Voice Code Custom Setting. 13 | 14 | A voice code can help identify the current user. The first time someone uses an Alexa skill with a voice code requirement, the user will be able to set a 4-digit code. You store the code in a custom setting in Salesforce. 15 | 16 | ### Create a protected custom setting. 17 | 1. Enter **Custom Settings** into the Quick Find box and then select **Custom Settings**. 18 | 2. Click **New**. 19 | 3. In the new Custom Setting Definition form, fill in: 20 | * Label: **Voice Code** 21 | * Object name: **Voice_Code** 22 | * Setting Type: **List** 23 | * Visibility: **Protected** 24 | * Click **Save**. 25 | 4. In the Voice Code Custom Settings Definition page, click **New** in the Custom Fields table. 26 | 5. Choose type **Text**. 27 | 6. Click **Next**. 28 | 7. In the New Custom Field form, fill in: 29 | * Field Label: **code** 30 | * Length: **60** 31 | * Field Name: **code** 32 | * Required: **Check the box** to require a value when saving a record 33 | * Click **Next**. 34 | 8. Click **Save**. 35 | 36 | ## Extra Credit 37 | 38 | To earn a Trailhead badge, go back and finish up unit 3 in the [Trailhead Playground Management Module](https://trailhead.salesforce.com/modules/trailhead_playground_management) . 39 | 40 | [![Next](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/button-next._TTH_.png)](./2-deploy.md) 41 | -------------------------------------------------------------------------------- /instructions/2-deploy.md: -------------------------------------------------------------------------------- 1 | # Deploy 2 | 3 | [![Salesforce Setup](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-1-done._TTH_.png)](./1-salesforce-setup.md)[![Deploy](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-2-on._TTH_.png)](./2-deploy.md)[![Account Linking](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-3-off._TTH_.png)](./3-account-linking.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-4-off._TTH_.png)](./4-testing.md)[![Distribute Private Skills](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-5-off._TTH_.png)](./5-distribute-private-skills.md) 4 | 5 | ## Part 2: Customize and Deploy the Skill 6 | 7 | In this part, we will deploy our skill and create the AWS Lambda function that powers it. First, you need to update the constants.js file in order to match your setup. 8 | 9 | ### Setup (without AWS Serverless Application Repository) 10 | 11 | 1. ```./lambda/custom/constants.js``` 12 | 13 | Modify these values in constants.js file : Salesforce instance URL and the name of your Voice Code custom setting and field name. There are other fields here we will modify later, so this won't be the last time you will be edit this file. 14 | 15 | Note: You can also control this value via Lambda Environment Variables using the same names shown below. 16 | 17 | ```javascript 18 | // Salesforce Constants 19 | INSTANCE_URL: process.env.INSTANCE_URL || '', // TODO Set your own 20 | VOICE_CODE_OBJECT_NAME: process.env.VOICE_CODE_OBJECT_NAME || 'voice_code__c', 21 | VOICE_CODE_FIELD_NAME: process.env.VOICE_CODE_FIELD_NAME || 'code__c', 22 | ``` 23 | 24 | ### Setup (with AWS Serverless Application Repository) 25 | 26 | 1. If you've deployed the application using the Serverless Application Repository, you simply need to add the function name that was created to one of the configuration files. Skip to **Step 4**. 27 | 2. If you wish to deploy the skill function using the AWS Serverless Application Repository, find the application [skill-sample-nodejs-salesforce](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:473507220772:applications~skill-sample-nodejs-salesforce). 28 | 3. Follow the instructions to deploy the application, setting the required parameter for your Salesforce instance URL based off what you created in Part 1. 29 | 4. Open your [Lambda console](https://console.aws.amazon.com/lambda/home). 30 | 5. Find the function that is named something like this: **aws-serverless-repository-AlexaSalesforceFunction-** 31 | 6. Copy the full function name, including the stack name and generated ID at the end. 32 | 7. Open the ```.ask/config``` in the root directory of this project. 33 | 8. Modify the value of **"uri"** to be the function name you copied in step 6. 34 | 35 | ``` 36 | "apis": { 37 | "custom": { 38 | "endpoint": { 39 | "uri": "ask-custom-AlexaSalesforceDemo-default" 40 | } 41 | } 42 | ``` 43 | 44 | ### Deployment 45 | 46 | ASK will create the skill and the lambda function for you. The Lambda function will be created in ```us-east-1``` (Northern Virginia) by default. 47 | 48 | If you created the Lambda function already with the Serverless Application Repository deployment process, it will link the function to your skill. 49 | 50 | 1. You deploy the skill and the lambda function in one step : 51 | 52 | ``` 53 | $ ask deploy 54 | -------------------- Create Skill Project -------------------- 55 | ask profile for the deployment: default 56 | Skill Id: amzn1.ask.skill. 57 | Skill deployment finished. 58 | Model deployment finished. 59 | Lambda deployment finished. 60 | ``` 61 | 62 | 2. Make sure to save your skill ID returned in the previous output. We’ll use that often in the future steps. 63 | 64 | ### Grant Permission for Lambda to Call DynamoDB (without AWS Serverless Application Repository) 65 | 66 | The standard execution role that is used for Lambda doesn’t allow permission to access DynamoDB. In order to fix that, you need to add a policy to the role that runs the Lambda function. 67 | 68 | 1. From the AWS Console, type **IAM** in the AWS services search box at the top of the page. 69 | 2. Click **Roles**. 70 | 3. Find the role that was automatically created for this Lambda function. It should be called **ask-lambda-Salesforce-Demo**. Click on it. 71 | 4. In the Permissions tab, click **Attach policy**. 72 | 5. In the search box, search for **AmazonDynamoDBFullAccess** and then check the box next to the policy that shows up. 73 | 6. Click **Attach policy** at the bottom right. 74 | 75 | ### Enable Testing 76 | 77 | In order to test the skill before publishing, you need to enable testing on the Alexa Developer Console. 78 | 79 | You can directly jump to the page by substituting your Skill ID into the following URL: ```https://developer.amazon.com/alexa/console/ask/test//development/en_US``` 80 | 81 | ## Extra Credit 82 | 83 | ### Modify Lambda to Only Respond to Your Skill (optional but recommended) 84 | 85 | 1. Modify the following value in ```./lambda/custom/constants.js``` file using the skill ID you just obtained. You can also make this chance in the AWS Lambda console as an environment variable. 86 | 87 | ```javascript 88 | appId: process.env.SKILL_ID || '', 89 | ``` 90 | 2. Run the deploy command again to update your Lambda function with the latest change. 91 | 92 | ```bash 93 | $ ask deploy 94 | ``` 95 | 96 | [![Next](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/button-next._TTH_.png)](./3-account-linking.md) 97 | -------------------------------------------------------------------------------- /instructions/3-account-linking.md: -------------------------------------------------------------------------------- 1 | # Private Alexa Skill With Salesforce Integration 2 | 3 | [![Salesforce Setup](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-1-done._TTH_.png)](./1-salesforce-setup.md)[![Deploy](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-2-done._TTH_.png)](./2-deploy.md)[![Account Linking](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-3-on._TTH_.png)](./3-account-linking.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-4-off._TTH_.png)](./4-testing.md)[![Distribute Private Skills](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-5-off._TTH_.png)](./5-distribute-private-skills.md) 4 | 5 | ## Part 3: Account Linking 6 | 7 | ### Obtain Your Amazon Developer Account Linking Redirect URLs 8 | In order to set up your Salesforce Connected App, you need your Amazon Developer Account Linking Redirect URLs. This is specific to your Amazon Developer account. You do not need to save any settings on this page at this time. 9 | 10 | 1. Go to ```https://developer.amazon.com/alexa/console/ask//development/en_US/account-linking```. 11 | 2. Move the slider to the right of the question "Do you allow users to create an account or link to an existing account with you?" to the right to enable account linking. 12 | 3. Copy and store the three URLs that show next to Redirect URLs at the bottom of the page. They will look like this: 13 | * ```https://alexa.amazon.co.jp/api/skill/link/ ``` 14 | * ```https://layla.amazon.com/api/skill/link/ ``` 15 | * ```https://pitangui.amazon.com/api/skill/link/``` 16 | 17 | ### Create a Connected App in Salesforce 18 | 19 | 1. Launch your Trailhead Playground org and click the **Setup** icon in the top right, then select the Setup link. 20 | 2. Enter **App Manager** into the Quick Find box and then select **App Manager**. 21 | 3. Click **New Connected App**. 22 | 4. In the New Connected App form, fill in: 23 | * Basic Information: 24 | * Connected App Name: **Alexa Skill** 25 | * API Name: **Alexa_Skill** 26 | * Contact Email: **enter your email address** 27 | * API (Enable OAuth Settings): 28 | * Check **Enable OAuth Settings**. 29 | * For Callback URL, use the **Redirect URLs** from the previous step. 30 | * In Selected OAuth Scopes, select **Access and manage your data (api)**. 31 | * Click **Add**. 32 | * In Selected OAuth Scopes, select **Perform requests on your behalf at any time (refresh_token, offline_access)**. 33 | * Click **Add**. 34 | * Click **Save**. 35 | * Click **Continue**. 36 | 5. Click **App Manager** again. Locate your newly created Alexa Skill and click the dropdown arrow on the far right. Select **View**. 37 | 6. Copy and store the **Consumer Key** and the **Consumer Secret** (click the Click to reveal button to see the secret key). You will need this shortly. 38 | 39 | ### Update Your Skill to Link to Your Salesforce Org 40 | With the details set in your Trailhead Playground Org, we can set up account linking with the Alexa skill we created earlier. 41 | 42 | 1. Go back to the ASK CLI. Enter the following command and fill out the resulting entries using your Connected App settings: 43 | 44 | ``` 45 | $ ask api create-account-linking -s 46 | ? Authorization URL: https://login.salesforce.com/services/oauth2/authorize 47 | ? Client ID: 48 | ? Scopes(separate by comma): api,refresh_token 49 | ? Domains(separate by comma): 50 | ? Authorization Grant Type: AUTH_CODE 51 | ? Access Token URI: https://login.salesforce.com/services/oauth2/token 52 | ? Client Secret: [hidden] 53 | ? Client Authentication Scheme: REQUEST_BODY_CREDENTIALS 54 | ? Optional* Default Access Token Expiration Time In Seconds: 3600 55 | Account Linking created successfully. 56 | ``` 57 | 58 | 2. The **Default Access Token Expiration Time In Seconds** field is required for Salesforce account linking. If you notice that you need to constantly re-link your Salesforce account to use the skill, re-do the previous step. 59 | 60 | [![Next](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/button-next._TTH_.png)](./4-testing.md) 61 | -------------------------------------------------------------------------------- /instructions/4-testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | [![Salesforce Setup](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-1-done._TTH_.png)](./1-salesforce-setup.md)[![Deploy](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-2-done._TTH_.png)](./2-deploy.md)[![Account Linking](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-3-done._TTH_.png)](./3-account-linking.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-4-on._TTH_.png)](./4-testing.md)[![Distribute Private Skills](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-5-off._TTH_.png)](./5-distribute-private-skills.md) 4 | 5 | ## Part 4: Testing 6 | 7 | Now that you completed most of the setup, let's make sure everything is working. We'll show you first how to test using the command line, then validate the account linking flow, and finally interact with the skill to create a voice code and access Salesforce data. 8 | 9 | ### Simulate 10 | 11 | 1. Run the following command to execute a command against your skill: 12 | 13 | ``` 14 | $ ask simulate -l en-US -t "open salesforce demo" 15 | ✓ Simulation created for simulation id: 0c857923-0753-43a5-b44c-ee2fca137aab 16 | ◜ Waiting for simulation response{ 17 | "status": "SUCCESSFUL", 18 | "result": { 19 | ... 20 | ``` 21 | 22 | 2. Check for the output message to also see what Alexa would have said: 23 | 24 | ``` 25 | ... 26 | "outputSpeech": { 27 | "type": "SSML", 28 | "ssml": " A Salesforce account is required to use this skill. I've placed more information on a card in your Alexa app. " 29 | }, 30 | ... 31 | ``` 32 | 33 | ### Test the Linking Flow 34 | 35 | 1. Go to your Alexa app on your device (or go to https://alexa.amazon.com). 36 | 2. Click **Skills**. 37 | 3. Click **Your Skills**. 38 | 4. Click the **Dev Skills** header. 39 | 5. Find the **Salesforce Demo** skill and click it. 40 | 6. Click **Enable**. 41 | 42 | Your browser or device will then open a window to the Salesforce login screen. 43 | Enter your Trailhead Playground user credentials, and you should see a page letting you know your skill was successfully linked. 44 | 45 | ### Use the Skill 46 | 47 | 1. Try out the following request: **“Alexa, open Salesforce Demo”**. 48 | 2. Alexa will welcome you and ask you to create a voice code. 49 | 3. Choose a 4-digit code and speak it back. 50 | 4. Alexa will create the code and let you ask about your recent leads or opportunities. 51 | 5. Don’t forget to try changing your code! 52 | 6. Now try pulling up an opportunity. If you've created a Trailhead Playground, you should have an opportunity with this name: **"Select opportunity United Oil Installations"**. 53 | 7. Alexa will pull back the opportunity and display some of the opportunity details on a card. 54 | 8. You can now change a detail, such as the close date: **"Update close date to next Friday"**. 55 | 9. Alexa will make the change in Salesforce and update the card details. 56 | 57 | If at any point you aren’t sure what else you can ask, just say “Help”. 58 | 59 | [![Next](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/button-next._TTH_.png)](./5-distribute-private-skills.md) 60 | -------------------------------------------------------------------------------- /instructions/5-distribute-private-skills.md: -------------------------------------------------------------------------------- 1 | # Distribute Private Skills 2 | 3 | [![Salesforce Setup](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-1-done._TTH_.png)](./1-salesforce-setup.md)[![Deploy](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-2-done._TTH_.png)](./2-deploy.md)[![Account Linking](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-3-done._TTH_.png)](./3-account-linking.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-4-done._TTH_.png)](./4-testing.md)[![Distribute Private Skills](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/tutorial-page-marker-5-on._TTH_.png)](./5-distribute-private-skills.md) 4 | 5 | ## Part 5: Distribute Private Skills 6 | 7 | [Alexa for Business](https://aws.amazon.com/alexaforbusiness/) lets you use Alexa to voice-enable your workplace by providing the tools you need to manage Alexa devices, skills, and users at scale, and an API to build custom, context-aware voice skills for your organization. 8 | 9 | In this case, let's assume you know the AWS account ID of an Alexa for Business organization that you want to give this Salesforce skill to. 10 | 11 | ### Publish the Private Skill 12 | The ```skill.json``` that you used to create this skill is already marked as private. In this case, you need to submit the skill to move it to the ```Live``` stage. 13 | 14 | 1. You can confirm that the skill is marked to be published to Alexa for Business Organizations by going to ```https://developer.amazon.com/alexa/console/ask/publish/alexapublishing//development/en_US/availability```. 15 | 16 | 2. Click on the **Submission** option in the left menu bar. 17 | 3. Click **Submit for review** to submit your skill to the ```Live``` stage. 18 | 19 | After the skill has been submitted, it takes about 30 minutes to 2 hours to propagate the skill to the live stage. To grant access to an AWS account, the skill must be in the live stage. 20 | 21 | ### Grant Access to an AWS Account 22 | 23 | 1. Find your skill in the [Alexa Skills Kit Developer Console](https://developer.amazon.com/alexa/console/ask). 24 | 2. Make sure you see the skill has a **Status** of **Live**. 25 | 3. Click on the **Manage Access** link to the right of your Live skill. 26 | 4. Enter in the AWS account ID of the Alexa for Business organization you wish to grant access to, using the following format: ```arn:aws:iam:::root``` 27 | 5. Click **Add**. 28 | 6. Click **Save**. 29 | 30 | ## Distribute the Skill to Users 31 | 32 | The Alexa for Business account you gave access to will now be able to review and enable the skill for distribution to their enrolled users. For more information, see [Private Skills](https://docs.aws.amazon.com/a4b/latest/ag/private-skills.html) and [Managing Users](https://docs.aws.amazon.com/a4b/latest/ag/manage-users.html) in our [Alexa for Business documentation](https://docs.aws.amazon.com/a4b/latest/ag/what-is.html). 33 | 34 | With these steps, you have been able to create a private Alexa skill that allows you to interact with your Salesforce data. You were able to set up account linking and a personal voice code. Finally, you deployed the skill to an Alexa for Business organization. 35 | -------------------------------------------------------------------------------- /lambda/custom/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the MIT License. See the LICENSE accompanying this file 5 | * for the specific language governing permissions and limitations under 6 | * the License. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = Object.freeze({ 12 | 13 | // App-ID. TODO: set to your own Skill App ID from the developer portal. 14 | appId: process.env.SKILL_ID || '', 15 | 16 | // Salesforce Constants 17 | INSTANCE_URL: process.env.INSTANCE_URL || '', // TODO Set your own 18 | VOICE_CODE_OBJECT_NAME: process.env.VOICE_CODE_OBJECT_NAME || 'voice_code__c', 19 | VOICE_CODE_FIELD_NAME: process.env.VOICE_CODE_FIELD_NAME || 'code__c', 20 | 21 | // Custom Skill Settings 22 | dynamoDBTableName: 'Salesforce_Skill', 23 | CODE_TIMEOUT_MINUTES: 5, 24 | SALESFORCE_USER_ID: 'salesforceUserId', 25 | 26 | // Salesforce field names 27 | OPPORTUNITY_AMOUNT: "Amount", 28 | OPPORTUNITY_CLOSE_DATE: "CloseDate", 29 | OPPORTUNITY_STAGE_NAME: "StageName", 30 | 31 | // Attributes names 32 | ATTRIBUTES_ACCOUNT: 'account-id', 33 | ATTRIBUTES_CHANGED_CODE: 'CHANGED_CODE', 34 | ATTRIBUTES_CONFIRMED_CODE: 'CONFIRMED_CODE', 35 | ATTRIBUTES_CREATED_CODE: 'CREATED_CODE', 36 | ATTRIBUTES_HAS_CODE: 'HAS_CODE', 37 | ATTRIBUTES_LAST_REQUEST: 'lastRequest', 38 | ATTRIBUTES_NUM_ATTEMPTS: 'numAttempts', 39 | ATTRIBUTES_OPPORTUNITY_ID: 'opportunity-id', 40 | ATTRIBUTES_OPPORTUNITY_NAME: 'opportunity-name', 41 | ATTRIBUTES_USER_MESSAGING: 'userMessaging', 42 | 43 | // For code debugging 44 | DEBUG: true, 45 | 46 | // States for state handlers 47 | STATES: { 48 | START: '', 49 | HELP: '_HELP_MODE', 50 | CODE: '_WAITING_FOR_CODE_MODE', // User needs to provide a voice code 51 | CHANGE_CODE: '_CHANGE_CODE', // User wants to change their voice code, confirming current code 52 | NEW_CODE: '_NEW_CODE', // User wants to set their new voice code 53 | SECURE: '_SECURE' // voice code validated 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /lambda/custom/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | var Alexa = require('ask-sdk-v1adapter'); 21 | var constants = require('./constants'); 22 | var languageStrings = require('./resourceStrings'); 23 | var voiceCodeHandlers = require('./voiceCodeHandlers'); 24 | var secureHandlers = require('./secureHandlers'); 25 | 26 | exports.handler = function (event, context, callback) { 27 | const alexa = Alexa.handler(event, context); 28 | alexa.appId = constants.appId; 29 | alexa.debug = constants.DEBUG; 30 | alexa.dynamoDBTableName = constants.dynamoDBTableName; 31 | alexa.resources = languageStrings; 32 | alexa.registerHandlers( 33 | voiceCodeHandlers.newSessionHandlers, 34 | voiceCodeHandlers.codeStateHandlers, 35 | voiceCodeHandlers.changeCodeHandlers, 36 | voiceCodeHandlers.newCodeHandlers, 37 | voiceCodeHandlers.helpStateHandlers, 38 | secureHandlers 39 | ); 40 | if (alexa.debug) { 41 | console.log("\n" + "******************* REQUEST **********************"); 42 | console.log("\n" + JSON.stringify(event, null, 2)); 43 | } 44 | alexa.execute(); 45 | }; -------------------------------------------------------------------------------- /lambda/custom/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexa-salesforce-demo", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "alexa-salesforce-demo", 9 | "version": "1.0.0", 10 | "license": "Amazon Software License", 11 | "dependencies": { 12 | "ask-sdk": "^2.0.0", 13 | "ask-sdk-v1adapter": "^2.0.1", 14 | "bcryptjs": "^2.4.3", 15 | "nforce": "^1.10.0" 16 | } 17 | }, 18 | "node_modules/ajv": { 19 | "version": "6.12.6", 20 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 21 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 22 | "dependencies": { 23 | "fast-deep-equal": "^3.1.1", 24 | "fast-json-stable-stringify": "^2.0.0", 25 | "json-schema-traverse": "^0.4.1", 26 | "uri-js": "^4.2.2" 27 | }, 28 | "funding": { 29 | "type": "github", 30 | "url": "https://github.com/sponsors/epoberezkin" 31 | } 32 | }, 33 | "node_modules/asap": { 34 | "version": "2.0.6", 35 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 36 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 37 | }, 38 | "node_modules/ask-sdk": { 39 | "version": "2.8.0", 40 | "resolved": "https://registry.npmjs.org/ask-sdk/-/ask-sdk-2.8.0.tgz", 41 | "integrity": "sha512-E/H0MC4of2tSSkSWTNv8s2ydoTftN5WVjt5g4tJjx7T3Ly6ZgrMG/LlUOaJQ/47Vaqn2cYW3WyynaC81ew/kUA==", 42 | "dependencies": { 43 | "ask-sdk-core": "^2.8.0", 44 | "ask-sdk-dynamodb-persistence-adapter": "^2.8.0", 45 | "ask-sdk-model": "^1.9.0" 46 | } 47 | }, 48 | "node_modules/ask-sdk-core": { 49 | "version": "2.8.0", 50 | "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.8.0.tgz", 51 | "integrity": "sha512-yW5D+5Z8sKOAm/vVySTZkj7f1+LemBsOAjTEZQTK4MdS8Lp8xteg3cmuqcvCp3M8405K2LYd7TknCoEAs82HEA==", 52 | "dependencies": { 53 | "ask-sdk-runtime": "^2.8.0" 54 | }, 55 | "peerDependencies": { 56 | "ask-sdk-model": "^1.9.0" 57 | } 58 | }, 59 | "node_modules/ask-sdk-dynamodb-persistence-adapter": { 60 | "version": "2.8.0", 61 | "resolved": "https://registry.npmjs.org/ask-sdk-dynamodb-persistence-adapter/-/ask-sdk-dynamodb-persistence-adapter-2.8.0.tgz", 62 | "integrity": "sha512-A5o4nwVV4pr472zMVmW2soywMa6QMrZbf2BAQoMbhi6gsdUeWbFxkuqNNqHYtaq7Ku9lk1wCJc7Kade9VcWkOw==", 63 | "dependencies": { 64 | "aws-sdk": "^2.163.0" 65 | }, 66 | "peerDependencies": { 67 | "ask-sdk-core": "^2.0.0" 68 | } 69 | }, 70 | "node_modules/ask-sdk-model": { 71 | "version": "1.28.0", 72 | "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.28.0.tgz", 73 | "integrity": "sha512-T4qz/hbXlQ8t6XvJBB7+tkYxpLe9/Ic+MEWNYGKgwbWnIOi5SKpfkXTEvV/mOEMpLMKj8WC84j+nRAyVc63NXQ==" 74 | }, 75 | "node_modules/ask-sdk-runtime": { 76 | "version": "2.8.0", 77 | "resolved": "https://registry.npmjs.org/ask-sdk-runtime/-/ask-sdk-runtime-2.8.0.tgz", 78 | "integrity": "sha512-hsVDcFKcbuj2H7MuAUlW0RBCTF8QOXfVCsrWSIgV2ABi8+PyI0zGUe8TdkQOGsPrDAv1PIzOS42QYqxWwPsp2g==" 79 | }, 80 | "node_modules/ask-sdk-v1adapter": { 81 | "version": "2.8.0", 82 | "resolved": "https://registry.npmjs.org/ask-sdk-v1adapter/-/ask-sdk-v1adapter-2.8.0.tgz", 83 | "integrity": "sha512-/xi5hmZpWF2JgNodmboAYqVIFKen/TAK0xrcc7uoYlZNuOV3VND9tsdG4ULi3/rvgsVzfS+MGI2nE8ksqPozFQ==", 84 | "dependencies": { 85 | "i18next": "^3.4.1", 86 | "i18next-sprintf-postprocessor": "^0.2.2" 87 | }, 88 | "peerDependencies": { 89 | "ask-sdk": "^2.0.0" 90 | } 91 | }, 92 | "node_modules/asn1": { 93 | "version": "0.2.4", 94 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 95 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 96 | "dependencies": { 97 | "safer-buffer": "~2.1.0" 98 | } 99 | }, 100 | "node_modules/assert-plus": { 101 | "version": "1.0.0", 102 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 103 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", 104 | "engines": { 105 | "node": ">=0.8" 106 | } 107 | }, 108 | "node_modules/asynckit": { 109 | "version": "0.4.0", 110 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 111 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 112 | }, 113 | "node_modules/aws-sdk": { 114 | "version": "2.1030.0", 115 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1030.0.tgz", 116 | "integrity": "sha512-to0STOb8DsSGuSsUb/WCbg/UFnMGfIYavnJH5ZlRCHzvCFjTyR+vfE8ku+qIZvfFM4+5MNTQC/Oxfun2X/TuyA==", 117 | "dependencies": { 118 | "buffer": "4.9.2", 119 | "events": "1.1.1", 120 | "ieee754": "1.1.13", 121 | "jmespath": "0.15.0", 122 | "querystring": "0.2.0", 123 | "sax": "1.2.1", 124 | "url": "0.10.3", 125 | "uuid": "3.3.2", 126 | "xml2js": "0.4.19" 127 | }, 128 | "engines": { 129 | "node": ">= 10.0.0" 130 | } 131 | }, 132 | "node_modules/aws-sdk/node_modules/buffer": { 133 | "version": "4.9.2", 134 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", 135 | "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", 136 | "dependencies": { 137 | "base64-js": "^1.0.2", 138 | "ieee754": "^1.1.4", 139 | "isarray": "^1.0.0" 140 | } 141 | }, 142 | "node_modules/aws-sign2": { 143 | "version": "0.7.0", 144 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 145 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", 146 | "engines": { 147 | "node": "*" 148 | } 149 | }, 150 | "node_modules/aws4": { 151 | "version": "1.9.1", 152 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", 153 | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" 154 | }, 155 | "node_modules/base64-js": { 156 | "version": "1.3.1", 157 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 158 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 159 | }, 160 | "node_modules/bcrypt-pbkdf": { 161 | "version": "1.0.2", 162 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 163 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 164 | "dependencies": { 165 | "tweetnacl": "^0.14.3" 166 | } 167 | }, 168 | "node_modules/bcryptjs": { 169 | "version": "2.4.3", 170 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", 171 | "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" 172 | }, 173 | "node_modules/caseless": { 174 | "version": "0.12.0", 175 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 176 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 177 | }, 178 | "node_modules/combined-stream": { 179 | "version": "1.0.8", 180 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 181 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 182 | "dependencies": { 183 | "delayed-stream": "~1.0.0" 184 | }, 185 | "engines": { 186 | "node": ">= 0.8" 187 | } 188 | }, 189 | "node_modules/core-util-is": { 190 | "version": "1.0.2", 191 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 192 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 193 | }, 194 | "node_modules/csprng": { 195 | "version": "0.1.2", 196 | "resolved": "https://registry.npmjs.org/csprng/-/csprng-0.1.2.tgz", 197 | "integrity": "sha1-S8aPEvo2jSUqWYQcusqXSxirReI=", 198 | "dependencies": { 199 | "sequin": "*" 200 | }, 201 | "engines": { 202 | "node": ">=0.6.0" 203 | } 204 | }, 205 | "node_modules/dashdash": { 206 | "version": "1.14.1", 207 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 208 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 209 | "dependencies": { 210 | "assert-plus": "^1.0.0" 211 | }, 212 | "engines": { 213 | "node": ">=0.10" 214 | } 215 | }, 216 | "node_modules/delayed-stream": { 217 | "version": "1.0.0", 218 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 219 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 220 | "engines": { 221 | "node": ">=0.4.0" 222 | } 223 | }, 224 | "node_modules/ecc-jsbn": { 225 | "version": "0.1.2", 226 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 227 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 228 | "dependencies": { 229 | "jsbn": "~0.1.0", 230 | "safer-buffer": "^2.1.0" 231 | } 232 | }, 233 | "node_modules/events": { 234 | "version": "1.1.1", 235 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 236 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", 237 | "engines": { 238 | "node": ">=0.4.x" 239 | } 240 | }, 241 | "node_modules/extend": { 242 | "version": "3.0.2", 243 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 244 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 245 | }, 246 | "node_modules/extsprintf": { 247 | "version": "1.3.0", 248 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 249 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", 250 | "engines": [ 251 | "node >=0.6.0" 252 | ] 253 | }, 254 | "node_modules/fast-deep-equal": { 255 | "version": "3.1.1", 256 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 257 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" 258 | }, 259 | "node_modules/fast-json-stable-stringify": { 260 | "version": "2.1.0", 261 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 262 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 263 | }, 264 | "node_modules/faye": { 265 | "version": "1.2.4", 266 | "resolved": "https://registry.npmjs.org/faye/-/faye-1.2.4.tgz", 267 | "integrity": "sha1-l47YpY8dSB5cH5i6y4lZ3l7FxkM=", 268 | "dependencies": { 269 | "asap": "*", 270 | "csprng": "*", 271 | "faye-websocket": ">=0.9.1", 272 | "tough-cookie": "*", 273 | "tunnel-agent": "*" 274 | }, 275 | "engines": { 276 | "node": ">=0.6.0" 277 | } 278 | }, 279 | "node_modules/faye-websocket": { 280 | "version": "0.11.3", 281 | "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", 282 | "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", 283 | "dependencies": { 284 | "websocket-driver": ">=0.5.1" 285 | }, 286 | "engines": { 287 | "node": ">=0.8.0" 288 | } 289 | }, 290 | "node_modules/forever-agent": { 291 | "version": "0.6.1", 292 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 293 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", 294 | "engines": { 295 | "node": "*" 296 | } 297 | }, 298 | "node_modules/form-data": { 299 | "version": "2.3.3", 300 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 301 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 302 | "dependencies": { 303 | "asynckit": "^0.4.0", 304 | "combined-stream": "^1.0.6", 305 | "mime-types": "^2.1.12" 306 | }, 307 | "engines": { 308 | "node": ">= 0.12" 309 | } 310 | }, 311 | "node_modules/getpass": { 312 | "version": "0.1.7", 313 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 314 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 315 | "dependencies": { 316 | "assert-plus": "^1.0.0" 317 | } 318 | }, 319 | "node_modules/har-schema": { 320 | "version": "2.0.0", 321 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 322 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", 323 | "engines": { 324 | "node": ">=4" 325 | } 326 | }, 327 | "node_modules/har-validator": { 328 | "version": "5.1.3", 329 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 330 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 331 | "deprecated": "this library is no longer supported", 332 | "dependencies": { 333 | "ajv": "^6.5.5", 334 | "har-schema": "^2.0.0" 335 | }, 336 | "engines": { 337 | "node": ">=6" 338 | } 339 | }, 340 | "node_modules/http-parser-js": { 341 | "version": "0.4.10", 342 | "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", 343 | "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" 344 | }, 345 | "node_modules/http-signature": { 346 | "version": "1.2.0", 347 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 348 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 349 | "dependencies": { 350 | "assert-plus": "^1.0.0", 351 | "jsprim": "^1.2.2", 352 | "sshpk": "^1.7.0" 353 | }, 354 | "engines": { 355 | "node": ">=0.8", 356 | "npm": ">=1.3.7" 357 | } 358 | }, 359 | "node_modules/i18next": { 360 | "version": "3.5.2", 361 | "resolved": "https://registry.npmjs.org/i18next/-/i18next-3.5.2.tgz", 362 | "integrity": "sha1-kwOQ1cMYzqpIWLUt0OQOayA/n0E=" 363 | }, 364 | "node_modules/i18next-sprintf-postprocessor": { 365 | "version": "0.2.2", 366 | "resolved": "https://registry.npmjs.org/i18next-sprintf-postprocessor/-/i18next-sprintf-postprocessor-0.2.2.tgz", 367 | "integrity": "sha1-LkCfEENXk4Jpi2otpwzapVHWfqQ=" 368 | }, 369 | "node_modules/ieee754": { 370 | "version": "1.1.13", 371 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 372 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 373 | }, 374 | "node_modules/is-typedarray": { 375 | "version": "1.0.0", 376 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 377 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 378 | }, 379 | "node_modules/isarray": { 380 | "version": "1.0.0", 381 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 382 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 383 | }, 384 | "node_modules/isstream": { 385 | "version": "0.1.2", 386 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 387 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 388 | }, 389 | "node_modules/jmespath": { 390 | "version": "0.15.0", 391 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 392 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", 393 | "engines": { 394 | "node": ">= 0.6.0" 395 | } 396 | }, 397 | "node_modules/jsbn": { 398 | "version": "0.1.1", 399 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 400 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 401 | }, 402 | "node_modules/json-schema": { 403 | "version": "0.4.0", 404 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 405 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 406 | }, 407 | "node_modules/json-schema-traverse": { 408 | "version": "0.4.1", 409 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 410 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 411 | }, 412 | "node_modules/json-stringify-safe": { 413 | "version": "5.0.1", 414 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 415 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 416 | }, 417 | "node_modules/jsprim": { 418 | "version": "1.4.2", 419 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", 420 | "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 421 | "dependencies": { 422 | "assert-plus": "1.0.0", 423 | "extsprintf": "1.3.0", 424 | "json-schema": "0.4.0", 425 | "verror": "1.10.0" 426 | }, 427 | "engines": { 428 | "node": ">=0.6.0" 429 | } 430 | }, 431 | "node_modules/lodash": { 432 | "version": "4.17.21", 433 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 434 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 435 | }, 436 | "node_modules/mime": { 437 | "version": "1.4.1", 438 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 439 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", 440 | "bin": { 441 | "mime": "cli.js" 442 | } 443 | }, 444 | "node_modules/mime-db": { 445 | "version": "1.43.0", 446 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", 447 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", 448 | "engines": { 449 | "node": ">= 0.6" 450 | } 451 | }, 452 | "node_modules/mime-types": { 453 | "version": "2.1.26", 454 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", 455 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", 456 | "dependencies": { 457 | "mime-db": "1.43.0" 458 | }, 459 | "engines": { 460 | "node": ">= 0.6" 461 | } 462 | }, 463 | "node_modules/nforce": { 464 | "version": "1.12.2", 465 | "resolved": "https://registry.npmjs.org/nforce/-/nforce-1.12.2.tgz", 466 | "integrity": "sha512-w4lOTFsHG+0/o/CDqtZDgjfHhYoUlWg3sdQzxmDWw3RsDCam7yRU2hgH9u8afZrbvYocjp2gFTMzSuatVqhajA==", 467 | "dependencies": { 468 | "faye": "^1.2.4", 469 | "lodash": "^4.17.11", 470 | "mime": "1.4.1", 471 | "request": "^2.88.0" 472 | }, 473 | "engines": { 474 | "node": ">= 0.12" 475 | } 476 | }, 477 | "node_modules/oauth-sign": { 478 | "version": "0.9.0", 479 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 480 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 481 | "engines": { 482 | "node": "*" 483 | } 484 | }, 485 | "node_modules/performance-now": { 486 | "version": "2.1.0", 487 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 488 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 489 | }, 490 | "node_modules/psl": { 491 | "version": "1.8.0", 492 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 493 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" 494 | }, 495 | "node_modules/punycode": { 496 | "version": "1.3.2", 497 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 498 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 499 | }, 500 | "node_modules/qs": { 501 | "version": "6.5.2", 502 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 503 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", 504 | "engines": { 505 | "node": ">=0.6" 506 | } 507 | }, 508 | "node_modules/querystring": { 509 | "version": "0.2.0", 510 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 511 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", 512 | "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", 513 | "engines": { 514 | "node": ">=0.4.x" 515 | } 516 | }, 517 | "node_modules/request": { 518 | "version": "2.88.2", 519 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 520 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 521 | "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", 522 | "dependencies": { 523 | "aws-sign2": "~0.7.0", 524 | "aws4": "^1.8.0", 525 | "caseless": "~0.12.0", 526 | "combined-stream": "~1.0.6", 527 | "extend": "~3.0.2", 528 | "forever-agent": "~0.6.1", 529 | "form-data": "~2.3.2", 530 | "har-validator": "~5.1.3", 531 | "http-signature": "~1.2.0", 532 | "is-typedarray": "~1.0.0", 533 | "isstream": "~0.1.2", 534 | "json-stringify-safe": "~5.0.1", 535 | "mime-types": "~2.1.19", 536 | "oauth-sign": "~0.9.0", 537 | "performance-now": "^2.1.0", 538 | "qs": "~6.5.2", 539 | "safe-buffer": "^5.1.2", 540 | "tough-cookie": "~2.5.0", 541 | "tunnel-agent": "^0.6.0", 542 | "uuid": "^3.3.2" 543 | }, 544 | "engines": { 545 | "node": ">= 6" 546 | } 547 | }, 548 | "node_modules/request/node_modules/punycode": { 549 | "version": "2.1.1", 550 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 551 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 552 | "engines": { 553 | "node": ">=6" 554 | } 555 | }, 556 | "node_modules/request/node_modules/tough-cookie": { 557 | "version": "2.5.0", 558 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 559 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 560 | "dependencies": { 561 | "psl": "^1.1.28", 562 | "punycode": "^2.1.1" 563 | }, 564 | "engines": { 565 | "node": ">=0.8" 566 | } 567 | }, 568 | "node_modules/safe-buffer": { 569 | "version": "5.2.0", 570 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 571 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 572 | }, 573 | "node_modules/safer-buffer": { 574 | "version": "2.1.2", 575 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 576 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 577 | }, 578 | "node_modules/sax": { 579 | "version": "1.2.1", 580 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 581 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 582 | }, 583 | "node_modules/sequin": { 584 | "version": "0.1.1", 585 | "resolved": "https://registry.npmjs.org/sequin/-/sequin-0.1.1.tgz", 586 | "integrity": "sha1-XC04nWajg3NOqvvEXt6ywcsb5wE=", 587 | "engines": { 588 | "node": ">=0.4.0" 589 | } 590 | }, 591 | "node_modules/sshpk": { 592 | "version": "1.16.1", 593 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 594 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 595 | "dependencies": { 596 | "asn1": "~0.2.3", 597 | "assert-plus": "^1.0.0", 598 | "bcrypt-pbkdf": "^1.0.0", 599 | "dashdash": "^1.12.0", 600 | "ecc-jsbn": "~0.1.1", 601 | "getpass": "^0.1.1", 602 | "jsbn": "~0.1.0", 603 | "safer-buffer": "^2.0.2", 604 | "tweetnacl": "~0.14.0" 605 | }, 606 | "bin": { 607 | "sshpk-conv": "bin/sshpk-conv", 608 | "sshpk-sign": "bin/sshpk-sign", 609 | "sshpk-verify": "bin/sshpk-verify" 610 | }, 611 | "engines": { 612 | "node": ">=0.10.0" 613 | } 614 | }, 615 | "node_modules/tough-cookie": { 616 | "version": "4.0.0", 617 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", 618 | "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", 619 | "dependencies": { 620 | "psl": "^1.1.33", 621 | "punycode": "^2.1.1", 622 | "universalify": "^0.1.2" 623 | }, 624 | "engines": { 625 | "node": ">=6" 626 | } 627 | }, 628 | "node_modules/tough-cookie/node_modules/punycode": { 629 | "version": "2.1.1", 630 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 631 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 632 | "engines": { 633 | "node": ">=6" 634 | } 635 | }, 636 | "node_modules/tunnel-agent": { 637 | "version": "0.6.0", 638 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 639 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 640 | "dependencies": { 641 | "safe-buffer": "^5.0.1" 642 | }, 643 | "engines": { 644 | "node": "*" 645 | } 646 | }, 647 | "node_modules/tweetnacl": { 648 | "version": "0.14.5", 649 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 650 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 651 | }, 652 | "node_modules/universalify": { 653 | "version": "0.1.2", 654 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 655 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", 656 | "engines": { 657 | "node": ">= 4.0.0" 658 | } 659 | }, 660 | "node_modules/uri-js": { 661 | "version": "4.2.2", 662 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 663 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 664 | "dependencies": { 665 | "punycode": "^2.1.0" 666 | } 667 | }, 668 | "node_modules/uri-js/node_modules/punycode": { 669 | "version": "2.1.1", 670 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 671 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 672 | "engines": { 673 | "node": ">=6" 674 | } 675 | }, 676 | "node_modules/url": { 677 | "version": "0.10.3", 678 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 679 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 680 | "dependencies": { 681 | "punycode": "1.3.2", 682 | "querystring": "0.2.0" 683 | } 684 | }, 685 | "node_modules/uuid": { 686 | "version": "3.3.2", 687 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 688 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", 689 | "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", 690 | "bin": { 691 | "uuid": "bin/uuid" 692 | } 693 | }, 694 | "node_modules/verror": { 695 | "version": "1.10.0", 696 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 697 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 698 | "engines": [ 699 | "node >=0.6.0" 700 | ], 701 | "dependencies": { 702 | "assert-plus": "^1.0.0", 703 | "core-util-is": "1.0.2", 704 | "extsprintf": "^1.2.0" 705 | } 706 | }, 707 | "node_modules/websocket-driver": { 708 | "version": "0.7.3", 709 | "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", 710 | "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", 711 | "dependencies": { 712 | "http-parser-js": ">=0.4.0 <0.4.11", 713 | "safe-buffer": ">=5.1.0", 714 | "websocket-extensions": ">=0.1.1" 715 | }, 716 | "engines": { 717 | "node": ">=0.8.0" 718 | } 719 | }, 720 | "node_modules/websocket-extensions": { 721 | "version": "0.1.4", 722 | "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", 723 | "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", 724 | "engines": { 725 | "node": ">=0.8.0" 726 | } 727 | }, 728 | "node_modules/xml2js": { 729 | "version": "0.4.19", 730 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 731 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 732 | "dependencies": { 733 | "sax": ">=0.6.0", 734 | "xmlbuilder": "~9.0.1" 735 | } 736 | }, 737 | "node_modules/xmlbuilder": { 738 | "version": "9.0.7", 739 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 740 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", 741 | "engines": { 742 | "node": ">=4.0" 743 | } 744 | } 745 | }, 746 | "dependencies": { 747 | "ajv": { 748 | "version": "6.12.6", 749 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 750 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 751 | "requires": { 752 | "fast-deep-equal": "^3.1.1", 753 | "fast-json-stable-stringify": "^2.0.0", 754 | "json-schema-traverse": "^0.4.1", 755 | "uri-js": "^4.2.2" 756 | } 757 | }, 758 | "asap": { 759 | "version": "2.0.6", 760 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 761 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 762 | }, 763 | "ask-sdk": { 764 | "version": "2.8.0", 765 | "resolved": "https://registry.npmjs.org/ask-sdk/-/ask-sdk-2.8.0.tgz", 766 | "integrity": "sha512-E/H0MC4of2tSSkSWTNv8s2ydoTftN5WVjt5g4tJjx7T3Ly6ZgrMG/LlUOaJQ/47Vaqn2cYW3WyynaC81ew/kUA==", 767 | "requires": { 768 | "ask-sdk-core": "^2.8.0", 769 | "ask-sdk-dynamodb-persistence-adapter": "^2.8.0", 770 | "ask-sdk-model": "^1.9.0" 771 | } 772 | }, 773 | "ask-sdk-core": { 774 | "version": "2.8.0", 775 | "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.8.0.tgz", 776 | "integrity": "sha512-yW5D+5Z8sKOAm/vVySTZkj7f1+LemBsOAjTEZQTK4MdS8Lp8xteg3cmuqcvCp3M8405K2LYd7TknCoEAs82HEA==", 777 | "requires": { 778 | "ask-sdk-runtime": "^2.8.0" 779 | } 780 | }, 781 | "ask-sdk-dynamodb-persistence-adapter": { 782 | "version": "2.8.0", 783 | "resolved": "https://registry.npmjs.org/ask-sdk-dynamodb-persistence-adapter/-/ask-sdk-dynamodb-persistence-adapter-2.8.0.tgz", 784 | "integrity": "sha512-A5o4nwVV4pr472zMVmW2soywMa6QMrZbf2BAQoMbhi6gsdUeWbFxkuqNNqHYtaq7Ku9lk1wCJc7Kade9VcWkOw==", 785 | "requires": { 786 | "aws-sdk": "^2.163.0" 787 | } 788 | }, 789 | "ask-sdk-model": { 790 | "version": "1.28.0", 791 | "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.28.0.tgz", 792 | "integrity": "sha512-T4qz/hbXlQ8t6XvJBB7+tkYxpLe9/Ic+MEWNYGKgwbWnIOi5SKpfkXTEvV/mOEMpLMKj8WC84j+nRAyVc63NXQ==" 793 | }, 794 | "ask-sdk-runtime": { 795 | "version": "2.8.0", 796 | "resolved": "https://registry.npmjs.org/ask-sdk-runtime/-/ask-sdk-runtime-2.8.0.tgz", 797 | "integrity": "sha512-hsVDcFKcbuj2H7MuAUlW0RBCTF8QOXfVCsrWSIgV2ABi8+PyI0zGUe8TdkQOGsPrDAv1PIzOS42QYqxWwPsp2g==" 798 | }, 799 | "ask-sdk-v1adapter": { 800 | "version": "2.8.0", 801 | "resolved": "https://registry.npmjs.org/ask-sdk-v1adapter/-/ask-sdk-v1adapter-2.8.0.tgz", 802 | "integrity": "sha512-/xi5hmZpWF2JgNodmboAYqVIFKen/TAK0xrcc7uoYlZNuOV3VND9tsdG4ULi3/rvgsVzfS+MGI2nE8ksqPozFQ==", 803 | "requires": { 804 | "i18next": "^3.4.1", 805 | "i18next-sprintf-postprocessor": "^0.2.2" 806 | } 807 | }, 808 | "asn1": { 809 | "version": "0.2.4", 810 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 811 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 812 | "requires": { 813 | "safer-buffer": "~2.1.0" 814 | } 815 | }, 816 | "assert-plus": { 817 | "version": "1.0.0", 818 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 819 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 820 | }, 821 | "asynckit": { 822 | "version": "0.4.0", 823 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 824 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 825 | }, 826 | "aws-sdk": { 827 | "version": "2.1030.0", 828 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1030.0.tgz", 829 | "integrity": "sha512-to0STOb8DsSGuSsUb/WCbg/UFnMGfIYavnJH5ZlRCHzvCFjTyR+vfE8ku+qIZvfFM4+5MNTQC/Oxfun2X/TuyA==", 830 | "requires": { 831 | "buffer": "4.9.2", 832 | "events": "1.1.1", 833 | "ieee754": "1.1.13", 834 | "jmespath": "0.15.0", 835 | "querystring": "0.2.0", 836 | "sax": "1.2.1", 837 | "url": "0.10.3", 838 | "uuid": "3.3.2", 839 | "xml2js": "0.4.19" 840 | }, 841 | "dependencies": { 842 | "buffer": { 843 | "version": "4.9.2", 844 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", 845 | "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", 846 | "requires": { 847 | "base64-js": "^1.0.2", 848 | "ieee754": "^1.1.4", 849 | "isarray": "^1.0.0" 850 | } 851 | } 852 | } 853 | }, 854 | "aws-sign2": { 855 | "version": "0.7.0", 856 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 857 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 858 | }, 859 | "aws4": { 860 | "version": "1.9.1", 861 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", 862 | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" 863 | }, 864 | "base64-js": { 865 | "version": "1.3.1", 866 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 867 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 868 | }, 869 | "bcrypt-pbkdf": { 870 | "version": "1.0.2", 871 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 872 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 873 | "requires": { 874 | "tweetnacl": "^0.14.3" 875 | } 876 | }, 877 | "bcryptjs": { 878 | "version": "2.4.3", 879 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", 880 | "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" 881 | }, 882 | "caseless": { 883 | "version": "0.12.0", 884 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 885 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 886 | }, 887 | "combined-stream": { 888 | "version": "1.0.8", 889 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 890 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 891 | "requires": { 892 | "delayed-stream": "~1.0.0" 893 | } 894 | }, 895 | "core-util-is": { 896 | "version": "1.0.2", 897 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 898 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 899 | }, 900 | "csprng": { 901 | "version": "0.1.2", 902 | "resolved": "https://registry.npmjs.org/csprng/-/csprng-0.1.2.tgz", 903 | "integrity": "sha1-S8aPEvo2jSUqWYQcusqXSxirReI=", 904 | "requires": { 905 | "sequin": "*" 906 | } 907 | }, 908 | "dashdash": { 909 | "version": "1.14.1", 910 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 911 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 912 | "requires": { 913 | "assert-plus": "^1.0.0" 914 | } 915 | }, 916 | "delayed-stream": { 917 | "version": "1.0.0", 918 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 919 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 920 | }, 921 | "ecc-jsbn": { 922 | "version": "0.1.2", 923 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 924 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 925 | "requires": { 926 | "jsbn": "~0.1.0", 927 | "safer-buffer": "^2.1.0" 928 | } 929 | }, 930 | "events": { 931 | "version": "1.1.1", 932 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 933 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 934 | }, 935 | "extend": { 936 | "version": "3.0.2", 937 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 938 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 939 | }, 940 | "extsprintf": { 941 | "version": "1.3.0", 942 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 943 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 944 | }, 945 | "fast-deep-equal": { 946 | "version": "3.1.1", 947 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 948 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" 949 | }, 950 | "fast-json-stable-stringify": { 951 | "version": "2.1.0", 952 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 953 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 954 | }, 955 | "faye": { 956 | "version": "1.2.4", 957 | "resolved": "https://registry.npmjs.org/faye/-/faye-1.2.4.tgz", 958 | "integrity": "sha1-l47YpY8dSB5cH5i6y4lZ3l7FxkM=", 959 | "requires": { 960 | "asap": "*", 961 | "csprng": "*", 962 | "faye-websocket": ">=0.9.1", 963 | "tough-cookie": "*", 964 | "tunnel-agent": "*" 965 | } 966 | }, 967 | "faye-websocket": { 968 | "version": "0.11.3", 969 | "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", 970 | "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", 971 | "requires": { 972 | "websocket-driver": ">=0.5.1" 973 | } 974 | }, 975 | "forever-agent": { 976 | "version": "0.6.1", 977 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 978 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 979 | }, 980 | "form-data": { 981 | "version": "2.3.3", 982 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 983 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 984 | "requires": { 985 | "asynckit": "^0.4.0", 986 | "combined-stream": "^1.0.6", 987 | "mime-types": "^2.1.12" 988 | } 989 | }, 990 | "getpass": { 991 | "version": "0.1.7", 992 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 993 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 994 | "requires": { 995 | "assert-plus": "^1.0.0" 996 | } 997 | }, 998 | "har-schema": { 999 | "version": "2.0.0", 1000 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 1001 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 1002 | }, 1003 | "har-validator": { 1004 | "version": "5.1.3", 1005 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 1006 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 1007 | "requires": { 1008 | "ajv": "^6.5.5", 1009 | "har-schema": "^2.0.0" 1010 | } 1011 | }, 1012 | "http-parser-js": { 1013 | "version": "0.4.10", 1014 | "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", 1015 | "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" 1016 | }, 1017 | "http-signature": { 1018 | "version": "1.2.0", 1019 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 1020 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 1021 | "requires": { 1022 | "assert-plus": "^1.0.0", 1023 | "jsprim": "^1.2.2", 1024 | "sshpk": "^1.7.0" 1025 | } 1026 | }, 1027 | "i18next": { 1028 | "version": "3.5.2", 1029 | "resolved": "https://registry.npmjs.org/i18next/-/i18next-3.5.2.tgz", 1030 | "integrity": "sha1-kwOQ1cMYzqpIWLUt0OQOayA/n0E=" 1031 | }, 1032 | "i18next-sprintf-postprocessor": { 1033 | "version": "0.2.2", 1034 | "resolved": "https://registry.npmjs.org/i18next-sprintf-postprocessor/-/i18next-sprintf-postprocessor-0.2.2.tgz", 1035 | "integrity": "sha1-LkCfEENXk4Jpi2otpwzapVHWfqQ=" 1036 | }, 1037 | "ieee754": { 1038 | "version": "1.1.13", 1039 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 1040 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 1041 | }, 1042 | "is-typedarray": { 1043 | "version": "1.0.0", 1044 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 1045 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 1046 | }, 1047 | "isarray": { 1048 | "version": "1.0.0", 1049 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1050 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 1051 | }, 1052 | "isstream": { 1053 | "version": "0.1.2", 1054 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 1055 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 1056 | }, 1057 | "jmespath": { 1058 | "version": "0.15.0", 1059 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 1060 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 1061 | }, 1062 | "jsbn": { 1063 | "version": "0.1.1", 1064 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 1065 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 1066 | }, 1067 | "json-schema": { 1068 | "version": "0.4.0", 1069 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 1070 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 1071 | }, 1072 | "json-schema-traverse": { 1073 | "version": "0.4.1", 1074 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1075 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 1076 | }, 1077 | "json-stringify-safe": { 1078 | "version": "5.0.1", 1079 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 1080 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 1081 | }, 1082 | "jsprim": { 1083 | "version": "1.4.2", 1084 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", 1085 | "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 1086 | "requires": { 1087 | "assert-plus": "1.0.0", 1088 | "extsprintf": "1.3.0", 1089 | "json-schema": "0.4.0", 1090 | "verror": "1.10.0" 1091 | } 1092 | }, 1093 | "lodash": { 1094 | "version": "4.17.21", 1095 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 1096 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 1097 | }, 1098 | "mime": { 1099 | "version": "1.4.1", 1100 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 1101 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 1102 | }, 1103 | "mime-db": { 1104 | "version": "1.43.0", 1105 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", 1106 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" 1107 | }, 1108 | "mime-types": { 1109 | "version": "2.1.26", 1110 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", 1111 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", 1112 | "requires": { 1113 | "mime-db": "1.43.0" 1114 | } 1115 | }, 1116 | "nforce": { 1117 | "version": "1.12.2", 1118 | "resolved": "https://registry.npmjs.org/nforce/-/nforce-1.12.2.tgz", 1119 | "integrity": "sha512-w4lOTFsHG+0/o/CDqtZDgjfHhYoUlWg3sdQzxmDWw3RsDCam7yRU2hgH9u8afZrbvYocjp2gFTMzSuatVqhajA==", 1120 | "requires": { 1121 | "faye": "^1.2.4", 1122 | "lodash": "^4.17.11", 1123 | "mime": "1.4.1", 1124 | "request": "^2.88.0" 1125 | } 1126 | }, 1127 | "oauth-sign": { 1128 | "version": "0.9.0", 1129 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 1130 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 1131 | }, 1132 | "performance-now": { 1133 | "version": "2.1.0", 1134 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 1135 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 1136 | }, 1137 | "psl": { 1138 | "version": "1.8.0", 1139 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 1140 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" 1141 | }, 1142 | "punycode": { 1143 | "version": "1.3.2", 1144 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 1145 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 1146 | }, 1147 | "qs": { 1148 | "version": "6.5.2", 1149 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 1150 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 1151 | }, 1152 | "querystring": { 1153 | "version": "0.2.0", 1154 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 1155 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 1156 | }, 1157 | "request": { 1158 | "version": "2.88.2", 1159 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 1160 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 1161 | "requires": { 1162 | "aws-sign2": "~0.7.0", 1163 | "aws4": "^1.8.0", 1164 | "caseless": "~0.12.0", 1165 | "combined-stream": "~1.0.6", 1166 | "extend": "~3.0.2", 1167 | "forever-agent": "~0.6.1", 1168 | "form-data": "~2.3.2", 1169 | "har-validator": "~5.1.3", 1170 | "http-signature": "~1.2.0", 1171 | "is-typedarray": "~1.0.0", 1172 | "isstream": "~0.1.2", 1173 | "json-stringify-safe": "~5.0.1", 1174 | "mime-types": "~2.1.19", 1175 | "oauth-sign": "~0.9.0", 1176 | "performance-now": "^2.1.0", 1177 | "qs": "~6.5.2", 1178 | "safe-buffer": "^5.1.2", 1179 | "tough-cookie": "~2.5.0", 1180 | "tunnel-agent": "^0.6.0", 1181 | "uuid": "^3.3.2" 1182 | }, 1183 | "dependencies": { 1184 | "punycode": { 1185 | "version": "2.1.1", 1186 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1187 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 1188 | }, 1189 | "tough-cookie": { 1190 | "version": "2.5.0", 1191 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 1192 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 1193 | "requires": { 1194 | "psl": "^1.1.28", 1195 | "punycode": "^2.1.1" 1196 | } 1197 | } 1198 | } 1199 | }, 1200 | "safe-buffer": { 1201 | "version": "5.2.0", 1202 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 1203 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 1204 | }, 1205 | "safer-buffer": { 1206 | "version": "2.1.2", 1207 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1208 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1209 | }, 1210 | "sax": { 1211 | "version": "1.2.1", 1212 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 1213 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 1214 | }, 1215 | "sequin": { 1216 | "version": "0.1.1", 1217 | "resolved": "https://registry.npmjs.org/sequin/-/sequin-0.1.1.tgz", 1218 | "integrity": "sha1-XC04nWajg3NOqvvEXt6ywcsb5wE=" 1219 | }, 1220 | "sshpk": { 1221 | "version": "1.16.1", 1222 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 1223 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 1224 | "requires": { 1225 | "asn1": "~0.2.3", 1226 | "assert-plus": "^1.0.0", 1227 | "bcrypt-pbkdf": "^1.0.0", 1228 | "dashdash": "^1.12.0", 1229 | "ecc-jsbn": "~0.1.1", 1230 | "getpass": "^0.1.1", 1231 | "jsbn": "~0.1.0", 1232 | "safer-buffer": "^2.0.2", 1233 | "tweetnacl": "~0.14.0" 1234 | } 1235 | }, 1236 | "tough-cookie": { 1237 | "version": "4.0.0", 1238 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", 1239 | "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", 1240 | "requires": { 1241 | "psl": "^1.1.33", 1242 | "punycode": "^2.1.1", 1243 | "universalify": "^0.1.2" 1244 | }, 1245 | "dependencies": { 1246 | "punycode": { 1247 | "version": "2.1.1", 1248 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1249 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 1250 | } 1251 | } 1252 | }, 1253 | "tunnel-agent": { 1254 | "version": "0.6.0", 1255 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1256 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1257 | "requires": { 1258 | "safe-buffer": "^5.0.1" 1259 | } 1260 | }, 1261 | "tweetnacl": { 1262 | "version": "0.14.5", 1263 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1264 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 1265 | }, 1266 | "universalify": { 1267 | "version": "0.1.2", 1268 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 1269 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" 1270 | }, 1271 | "uri-js": { 1272 | "version": "4.2.2", 1273 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1274 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1275 | "requires": { 1276 | "punycode": "^2.1.0" 1277 | }, 1278 | "dependencies": { 1279 | "punycode": { 1280 | "version": "2.1.1", 1281 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1282 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 1283 | } 1284 | } 1285 | }, 1286 | "url": { 1287 | "version": "0.10.3", 1288 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 1289 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 1290 | "requires": { 1291 | "punycode": "1.3.2", 1292 | "querystring": "0.2.0" 1293 | } 1294 | }, 1295 | "uuid": { 1296 | "version": "3.3.2", 1297 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 1298 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 1299 | }, 1300 | "verror": { 1301 | "version": "1.10.0", 1302 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1303 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1304 | "requires": { 1305 | "assert-plus": "^1.0.0", 1306 | "core-util-is": "1.0.2", 1307 | "extsprintf": "^1.2.0" 1308 | } 1309 | }, 1310 | "websocket-driver": { 1311 | "version": "0.7.3", 1312 | "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", 1313 | "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", 1314 | "requires": { 1315 | "http-parser-js": ">=0.4.0 <0.4.11", 1316 | "safe-buffer": ">=5.1.0", 1317 | "websocket-extensions": ">=0.1.1" 1318 | } 1319 | }, 1320 | "websocket-extensions": { 1321 | "version": "0.1.4", 1322 | "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", 1323 | "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" 1324 | }, 1325 | "xml2js": { 1326 | "version": "0.4.19", 1327 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 1328 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 1329 | "requires": { 1330 | "sax": ">=0.6.0", 1331 | "xmlbuilder": "~9.0.1" 1332 | } 1333 | }, 1334 | "xmlbuilder": { 1335 | "version": "9.0.7", 1336 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 1337 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 1338 | } 1339 | } 1340 | } 1341 | -------------------------------------------------------------------------------- /lambda/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexa-salesforce-demo", 3 | "version": "1.0.0", 4 | "description": "A sample skill that connects Alexa and Salesforce", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon.com, Inc.", 10 | "license": "Amazon Software License", 11 | "dependencies": { 12 | "ask-sdk": "^2.0.0", 13 | "ask-sdk-v1adapter": "^2.0.1", 14 | "bcryptjs": "^2.4.3", 15 | "nforce": "^1.10.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lambda/custom/resourceStrings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | 18 | var languageString = { 19 | "en": { 20 | "translation": { 21 | /* General messaging */ 22 | "CANCEL_MESSAGE": "Ok, I've cleared your session details. Goodbye.", 23 | "LIKE_TO_DO": "What would you like to do?", 24 | "PROMPT": "How else can I help?", 25 | "STOP_MESSAGE": "Ok, bye!", 26 | "WELCOME_HAS_CODE": "Please say your 4-digit voice code to get started. ", 27 | "WELCOME_MESSAGE": "Welcome to the sample Alexa skill with Salesforce integration. ", 28 | "WELCOME_NO_CODE": "To use this skill, you need to set a 4-digit voice code first. Please say a voice code now. ", 29 | "WELCOME_SKILL": "Your voice code is still valid. ", 30 | "WELCOME_SUCCESS_CODE": "Great, I verified your identity using your voice code. ", 31 | "WELCOME_SUCCESS_NEW_CODE": "I've changed your voice code. ", 32 | 33 | /* Help messaging */ 34 | "HELP_MESSAGE": "This skill can be used to see how Alexa can work with Salesforce to set a voice code, and how to access Salesforce data through the account linking process. Try asking for a recent lead, or opportunity. For testing the voice code process, try asking me to start over, so you will have to authenticate again. You can also ask to change your voice code. How can I help? ", 35 | "SHORT_HELP": "You can ask about your most recent lead, or I can find an opportunity by name. What would you want to do?", 36 | 37 | /* Slot Elicitation */ 38 | "OPPORTUNITY_VALUE": "What's the new amount?'", 39 | "OPPORTUNITY_DATE": "What's the new close date?'", 40 | "OPPORTUNITY_STAGE": "What's the new stage?'", 41 | 42 | /* Account/Voice Code Related messages */ 43 | "ACCOUNT_RELINK_MESSAGE": "You need to relink your Salesforce account in order to use this skill. I've placed more information on a card in your Alexa app. ", 44 | "ACCOUNT_REQUIRED_MESSAGE": "A Salesforce account is required to use this skill. I've placed more information on a card in your Alexa app. ", 45 | "ACCOUNT_REQUIRED_CARD": "Relink your account. To relink your account, open the skill within the Alexa App and click re-link account. ", 46 | "CODE_REPEAT_REQUEST": "Please try your code one more time. ", 47 | "CHANGE_NEW_CODE": "Say your new, 4-digit voice code now. ", 48 | "CHANGE_PROVIDE_CODE": "Before proceeding with a voice code change, please say your current voice code. ", 49 | "CODE_REQUEST": "Please say your voice code. ", 50 | "CODE_SET": "I've saved your voice code. You'll need to use it the next time you use this skill, so don't forget it! ", 51 | 52 | /* Salesforce related messages */ 53 | "LEAD": "Your most recent lead is for %s from %s. ", 54 | "LEAD_NOT_FOUND": "I didn't find any leads. What else can I do?", 55 | "OPPORTUNITY": "Your most recent opportunity that's not closed is for %s and it's in the %s stage. ", 56 | "OPPORTUNITY_CARD": "ID: %s\nName: %s\nClose Date: %s\nStage: %s\nAmount: %s", 57 | "OPPORTUNITY_CARD_TITLE": "Opportunity Details", 58 | "OPPORTUNITY_ERROR": "I ran into an error when trying to update your opportunity. Please try again later. ", 59 | "OPPORTUNITY_NOT_FOUND": "I didn't find any opportunities. Can you try another name? ", 60 | "OPPORTUNITY_PREVIOUS": "Here is the last Opportunity you selected, %s. To remove this, just tell me to cancel. Otherwise, you can make updates to the stage, amount, or close date.", 61 | "OPPORTUNITY_SELECTED": "I found an Opportunity called %s. You can make updates to the stage, amount, or close date.", 62 | "OPPORTUNITY_UPDATE": "I've made the update. ", 63 | 64 | /* Error messages */ 65 | "UNKNOWN_SALESFORCE_ERROR": "I encountered an error when trying reach Salesforce. Please try again later.", 66 | "UNKNOWN_ERROR": "I ran into a problem with that request. Please try again later. ", 67 | } 68 | } 69 | }; 70 | 71 | module.exports = languageString; -------------------------------------------------------------------------------- /lambda/custom/salesforce.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | const constants = require('./constants'); 21 | const nforce = require('nforce'); 22 | 23 | /* 24 | These are set to NA as they are not used, due to the fact that we are using 25 | Alexa's account linking process to obtain an acess token, not the default 26 | nforce createConnection and authenticate methods. 27 | */ 28 | const org = nforce.createConnection({ 29 | clientId: "NA", 30 | clientSecret: "NA", 31 | redirectUri: "NA" 32 | }); 33 | 34 | const sf = { 35 | getIdentity: function (accessToken, callback, origContext) { 36 | org.getIdentity({ oauth: getOauthObject(accessToken) }, callback.bind(origContext)); 37 | }, 38 | getVoiceCode: function (userId, accessToken, callback, origContext) { 39 | const q = `Select ${constants.VOICE_CODE_FIELD_NAME} From ${constants.VOICE_CODE_OBJECT_NAME} Where Name = '${userId}' LIMIT 1`; 40 | org.query({ oauth: getOauthObject(accessToken), query: q }, callback.bind(origContext)); 41 | }, 42 | query: function (query, accessToken, callback, origContext) { 43 | const q = query; 44 | org.query({ oauth: getOauthObject(accessToken), query: q }, callback.bind(origContext)); 45 | }, 46 | updateVoiceCode: function (code, userId, accessToken, callback, origContext) { 47 | const q = `Select Id From ${constants.VOICE_CODE_OBJECT_NAME} Where Name = '${userId}' LIMIT 1`; 48 | 49 | org.query({ oauth: getOauthObject(accessToken), query: q }).then(function (resp) { 50 | const updated_code = nforce.createSObject(constants.VOICE_CODE_OBJECT_NAME); 51 | updated_code.set(constants.VOICE_CODE_FIELD_NAME, code); 52 | updated_code.set("Id", resp.records[0]._fields.id); 53 | updated_code.set("Name", userId); 54 | org.update({ sobject: updated_code, oauth: getOauthObject(accessToken) }, callback.bind(origContext)); 55 | }); 56 | }, 57 | createVoiceCode: function (code, userId, accessToken, callback, origContext) { 58 | const new_code = nforce.createSObject(constants.VOICE_CODE_OBJECT_NAME); 59 | new_code.set(constants.VOICE_CODE_FIELD_NAME, code); 60 | new_code.set("Name", userId); 61 | org.insert({ sobject: new_code, oauth: getOauthObject(accessToken) }, callback.bind(origContext)); 62 | }, 63 | updateOpportunity: function (id, value, fieldName, userId, accessToken, callback, origContext) { 64 | if (id && userId && fieldName && value && accessToken) { 65 | const update_opportunity = nforce.createSObject("Opportunity"); 66 | update_opportunity.set("Id", id); 67 | update_opportunity.set(fieldName, value); 68 | org.update({ sobject: update_opportunity, oauth: getOauthObject(accessToken) }, callback.bind(origContext)); 69 | } else { 70 | console.error(`missing value for one value: id: ${id}, userId: ${userId}, fieldName: ${fieldName}, value: ${value}, accessToken: ${accessToken}.`); 71 | callback.call(origContext, "Error, fields missing"); 72 | } 73 | } 74 | } 75 | 76 | module.exports = sf; 77 | 78 | function getOauthObject(accessToken) { 79 | // Construct our OAuth token based on the access token we were provided from Alexa 80 | const oauth = {}; 81 | oauth.access_token = accessToken; 82 | oauth.instance_url = constants.INSTANCE_URL; 83 | return oauth; 84 | } -------------------------------------------------------------------------------- /lambda/custom/secureHandlers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | var Alexa = require('ask-sdk-v1adapter'); 21 | var bcrypt = require('bcryptjs'); 22 | 23 | const constants = require('./constants'); 24 | const voiceCodeHandlers = require('./voiceCodeHandlers'); 25 | const sf = require('./salesforce'); 26 | 27 | const imageObj = { 28 | largeImageUrl: 'https://s3.amazonaws.com/alexa-salesforce-demo-skill-images/sales_image.png' 29 | }; 30 | 31 | const handlers = Alexa.CreateStateHandler(constants.STATES.SECURE, { 32 | 'LaunchRequest': function () { 33 | if (preFunctions.call(this)) { 34 | let output = ''; 35 | 36 | // Get the messaging for the user depending on their state of their voice code 37 | if (this.attributes[constants.ATTRIBUTES_CHANGED_CODE]) { 38 | // Case where user just changed a code 39 | output = this.t("WELCOME_SUCCESS_NEW_CODE"); 40 | this.attributes[constants.ATTRIBUTES_CHANGED_CODE] = null; 41 | } else if (this.attributes[constants.ATTRIBUTES_CREATED_CODE]) { 42 | // Case where user just created a new voice code 43 | output = this.t("CODE_SET"); 44 | this.attributes[constants.ATTRIBUTES_CREATED_CODE] = null; 45 | } else if (this.attributes[constants.ATTRIBUTES_CONFIRMED_CODE]) { 46 | // Case where user confirmed code (coming from another handler) 47 | output = this.t("WELCOME_SUCCESS_CODE"); 48 | this.attributes[constants.ATTRIBUTES_CONFIRMED_CODE] = null; 49 | } else { 50 | // Case where user has a new session with a valid voice code 51 | output = this.t("WELCOME_MESSAGE") + this.t("WELCOME_SKILL") 52 | } 53 | 54 | // Include opportunity details if they have it 55 | if (this.attributes[constants.ATTRIBUTES_OPPORTUNITY_ID]) { 56 | // Store the output so far so we can use it in the callback for optimal messaging 57 | this.attributes[constants.ATTRIBUTES_USER_MESSAGING] = output; 58 | 59 | // Refresh the opportunity details 60 | const opportunityId = this.attributes[constants.ATTRIBUTES_OPPORTUNITY_ID]; 61 | const accessToken = this.event.session.user.accessToken; 62 | sf.query(`Select id, Name, StageName, CloseDate, Amount From Opportunity Where id='${opportunityId}' AND isclosed=false LIMIT 1`, accessToken, findOpportunity, this); 63 | } else { 64 | // In this case, we just add on the simple message to ask the user what they'd like to do. 65 | output += this.t("SHORT_HELP"); 66 | this.emit(":ask", output, this.t("SHORT_HELP")); 67 | } 68 | } 69 | }, 70 | 'AMAZON.StartOverIntent': function () { 71 | voiceCodeHandlers.resetAttributes(true, this.attributes); 72 | // Route the user back to the SetupAccount function in voiceCodeHandlers.js if they request to start over. 73 | this.handler.state = constants.STATES.CODE; 74 | this.emitWithState("SetupAccount"); 75 | }, 76 | 'RecentLead': function () { 77 | if (preFunctions.call(this)) { 78 | const accessToken = this.event.session.user.accessToken; 79 | sf.query("Select Name,Company From Lead ORDER BY CreatedDate DESC LIMIT 1", accessToken, (err, resp) => { 80 | if (!err) { 81 | if (resp.records) { 82 | const output = this.t("LEAD", resp.records[0]._fields.name, resp.records[0]._fields.company) + this.t("PROMPT"); 83 | this.emit(":ask", output, this.t("PROMPT")); 84 | } else { 85 | const output = this.t("LEAD_NOT_FOUND") + this.t("PROMPT"); 86 | this.emit(":ask", output, this.t("PROMPT")); 87 | } 88 | } else { 89 | console.log("Error in lead query call: " + JSON.stringify(err)); 90 | this.emit(":tell", this.t("UNKNOWN_SALESFORCE_ERROR")); 91 | } 92 | }); 93 | } 94 | }, 95 | 'RecentOpportunity': function () { 96 | if (preFunctions.call(this)) { 97 | const accessToken = this.event.session.user.accessToken; 98 | sf.query("Select Name,StageName From Opportunity Where StageName != 'Closed Won' ORDER BY CreatedDate DESC LIMIT 1", accessToken, (err, resp) => { 99 | if (!err) { 100 | console.log('Opportunity query succeeded! ' + JSON.stringify(resp)); 101 | 102 | if (resp.records) { 103 | var output = this.t("OPPORTUNITY", resp.records[0]._fields.name, resp.records[0]._fields.stagename) + this.t("PROMPT"); 104 | this.emit(":ask", output, this.t("PROMPT")); 105 | } else { 106 | var output = this.t("OPPORTUNITY_NOT_FOUND") + this.t("PROMPT"); 107 | this.emit(":ask", output, this.t("PROMPT")); 108 | } 109 | } else { 110 | console.log("Error in opportunity query call: " + JSON.stringify(err)); 111 | this.emit(":tell", this.t("UNKNOWN_SALESFORCE_ERROR")); 112 | } 113 | }); 114 | } 115 | }, 116 | 'UpdateOpportunityDateIntent': function () { 117 | console.log("UpdateOpportunityDateIntent function") 118 | if (preFunctions.call(this) && checkSlot.call(this, "opportunity_date", "OPPORTUNITY_DATE")) { 119 | const opportunityDate = getSlotValue(this.event.request.intent.slots.opportunity_date); 120 | const opportunityId = this.attributes[constants.ATTRIBUTES_OPPORTUNITY_ID]; 121 | const currentUserId = this.attributes[constants.SALESFORCE_USER_ID]; 122 | const accessToken = this.event.session.user.accessToken; 123 | 124 | console.log(`UpdateOpportunityDate \nopportunityDate: ${opportunityDate}, opportunityId: ${opportunityId}`); 125 | sf.updateOpportunity(opportunityId, opportunityDate, constants.OPPORTUNITY_CLOSE_DATE, currentUserId, accessToken, updateOpportunity, this); 126 | } 127 | }, 128 | 'UpdateOpportunityAmountIntent': function () { 129 | console.log("UpdateOpportunityAmountIntent function") 130 | if (preFunctions.call(this) && checkSlot.call(this, "opportunity_value", "OPPORTUNITY_VALUE")) { 131 | const opportunityValue = getSlotValue(this.event.request.intent.slots.opportunity_value); 132 | const opportunityId = this.attributes[constants.ATTRIBUTES_OPPORTUNITY_ID]; 133 | const currentUserId = this.attributes[constants.SALESFORCE_USER_ID]; 134 | const accessToken = this.event.session.user.accessToken; 135 | 136 | console.log(`UpdateOpportunityAmount \nopportunityValue: ${opportunityValue}, opportunityId: ${opportunityId}`); 137 | sf.updateOpportunity(opportunityId, opportunityValue, constants.OPPORTUNITY_AMOUNT, currentUserId, accessToken, updateOpportunity, this); 138 | } 139 | }, 140 | 'UpdateOpportunityStageIntent': function () { 141 | console.log("UpdateOpportunityStageIntent function") 142 | if (preFunctions.call(this) && checkSlot.call(this, "opportunity_stage", "OPPORTUNITY_STAGE")) { 143 | const opportunityStage = getSlotValue(this.event.request.intent.slots.opportunity_stage); 144 | const opportunityId = this.attributes[constants.ATTRIBUTES_OPPORTUNITY_ID]; 145 | const currentUserId = this.attributes[constants.SALESFORCE_USER_ID]; 146 | const accessToken = this.event.session.user.accessToken; 147 | 148 | console.log(`UpdateOpportunityStage \nopportunityStage: ${opportunityStage}, opportunityId: ${opportunityId}`); 149 | sf.updateOpportunity(opportunityId, opportunityStage, constants.OPPORTUNITY_STAGE_NAME, currentUserId, accessToken, updateOpportunity, this); 150 | } 151 | }, 152 | 'SelectOpportunityIntent': function () { 153 | console.log("SelectOpportunityIntent function"); 154 | if (preFunctions.call(this)) { 155 | const opportunityName = getSlotValue(this.event.request.intent.slots.opportunity_name); 156 | console.log(`opportunityName: ${opportunityName}`); 157 | const accessToken = this.event.session.user.accessToken; 158 | sf.query(`Select id, Name, StageName, CloseDate, Amount From Opportunity Where Name='${opportunityName}' AND isclosed=false LIMIT 1`, accessToken, findOpportunity, this); 159 | } 160 | }, 161 | 'ChangeCode': function () { 162 | // Route the user back to the PromptForCode function in the CHANGE_CODE state handler 163 | // in voiceCodeHandlers.js if the user wants to change their code. 164 | this.handler.state = constants.STATES.CHANGE_CODE; 165 | this.emitWithState("PromptForCode"); 166 | }, 167 | 'AMAZON.StopIntent': function () { 168 | this.emit(':tell', this.t("STOP_MESSAGE")); 169 | }, 170 | 'AMAZON.CancelIntent': function () { 171 | // Use this function to clear up and save any data needed between sessions 172 | this.attributes[constants.ATTRIBUTES_OPPORTUNITY_ID] = null; 173 | this.attributes[constants.ATTRIBUTES_OPPORTUNITY_NAME] = null; 174 | 175 | this.emit(":tell", this.t("CANCEL_MESSAGE")); 176 | }, 177 | 'SessionEndedRequest': function () { 178 | // Use this function to clear up and save any data needed between sessions 179 | this.emit('AMAZON.StopIntent'); 180 | }, 181 | 'AMAZON.HelpIntent': function () { 182 | // Route the user to the HELP state handler in voiceCodeHandlers.js 183 | this.handler.state = constants.STATES.HELP; 184 | this.emitWithState("helpTheUser"); 185 | }, 186 | 'Unhandled': function () { 187 | console.log("in secureHandler Unhandled"); 188 | // Route the user to the CODE state handler in voiceCodeHandlers.js 189 | this.handler.state = constants.STATES.CODE; 190 | this.emitWithState("UnhandledError"); 191 | } 192 | }); 193 | 194 | module.exports = handlers; 195 | 196 | /** 197 | * Collection of logic to be executed at the start of any secured intent 198 | */ 199 | const preFunctions = function () { 200 | // Extra debugging to get the value of the session attributes 201 | console.log(`DEBUG - attributes:\n${JSON.stringify(this.attributes)}`); 202 | let userValid = true; 203 | // Validate the voice code is within a given timeout window 204 | if (!voiceCodeHandlers.verifyVoiceCodeTimeout.call(this)) { 205 | console.log("User's voice code needs to be refreshed."); 206 | // Require the code to be spoken 207 | this.attributes[constants.ATTRIBUTES_LAST_REQUEST] = null; 208 | this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] = 0; 209 | this.handler.state = constants.STATES.CODE; 210 | this.emitWithState("SetupAccount"); 211 | userValid = false; 212 | } else { 213 | console.log("User's voice code is still within valid timeout limits."); 214 | // Make sure we're in the right secure state 215 | this.handler.state = constants.STATES.SECURE; 216 | } 217 | return userValid; 218 | } 219 | 220 | /** 221 | * Slot helper to elicit the slot if it doesn't exist 222 | */ 223 | const checkSlot = function(slotName, slotMessage) { 224 | const intentObj = this.event.request.intent; 225 | let slotValid = true; 226 | if (!intentObj.slots[slotName].value) { 227 | console.log(`Couldn't find a slot value for ${slotName}. Eliciting for this slot value.`); 228 | this.emit(":elicitSlot", slotName, this.t(slotMessage), this.t(slotMessage)); 229 | this.emit(":responseReady"); 230 | slotValid = false; 231 | } 232 | return slotValid; 233 | } 234 | 235 | /** 236 | * Obtains a slot value from entity resolution (if it matched a synonym) or just from the primary slot value 237 | * @returns value for a given slot input 238 | * */ 239 | const getSlotValue = function (slot) { 240 | let slotValue; 241 | console.log(`DEBUG - slot: ${JSON.stringify(slot)}`); 242 | if (slot && slot.resolutions && slot.resolutions.resolutionsPerAuthority && slot.resolutions.resolutionsPerAuthority.length > 0 && 243 | slot.resolutions.resolutionsPerAuthority[0].values && slot.resolutions.resolutionsPerAuthority[0].values.length > 0) { 244 | // For the purpose of this skill, we'll assume that resolutions mean we have one 245 | // canonical entry from one ER. It is possible, and likely, that real scnearios 246 | // have multiple canonical choices, but we're being simple for a demo. 247 | slotValue = slot.resolutions.resolutionsPerAuthority[0].values[0].value.name; 248 | console.log(`DEBUG - getSlotValue resolutions flow - slotValue: ${slotValue}`); 249 | } 250 | if (!slotValue && slot && slot.value) { 251 | // If we don't have any entity resolutions or if it didn't resolve to anything, just take the slot value (if it exists) 252 | slotValue = slot.value; 253 | console.log(`DEBUG - getSlotValue non-resolutions flow - slotValue: ${slotValue}`); 254 | } 255 | return slotValue; 256 | } 257 | 258 | /* 259 | * Find an opportunity callback function - processes a single opportunity that was selected by name. 260 | */ 261 | const findOpportunity = function (err, resp) { 262 | console.log("findOpportunity function"); 263 | if (!err) { 264 | console.log(`Opportunity query succeeded: ${JSON.stringify(resp)}`); 265 | if (resp.records.length > 0) { 266 | const opp = resp.records[0]._fields; 267 | const currentOppId = this.attributes[constants.ATTRIBUTES_OPPORTUNITY_ID]; 268 | let output = ''; 269 | 270 | if (currentOppId === opp.id) { 271 | console.log("DEBUG - Matched opportunity in session, giving sign in messaging"); 272 | output = this.attributes[constants.ATTRIBUTES_USER_MESSAGING]; 273 | this.attributes[constants.ATTRIBUTES_USER_MESSAGING] = null; 274 | output += this.t("OPPORTUNITY_PREVIOUS", opp.name); 275 | } else { 276 | console.log("DEBUG - No opportunity in session or doesn't match, giving look up opportunity messaging"); 277 | this.attributes[constants.ATTRIBUTES_OPPORTUNITY_ID] = opp.id; 278 | this.attributes[constants.ATTRIBUTES_OPPORTUNITY_NAME] = opp.name; 279 | output = this.t("OPPORTUNITY_SELECTED", opp.name); 280 | } 281 | 282 | this.response.speak(output) 283 | .cardRenderer(this.t("OPPORTUNITY_CARD_TITLE"), this.t("OPPORTUNITY_CARD", opp.id, opp.name, opp.closedate, opp.stagename, renderDollarAmount(opp.amount)), imageObj) 284 | .listen(this.t("LIKE_TO_DO")); 285 | this.emit(":responseReady"); 286 | } else { 287 | const output = this.t("OPPORTUNITY_NOT_FOUND"); 288 | this.emit(":ask", output, output); 289 | } 290 | } else { 291 | console.log(`Error in opportunity query call: ${JSON.stringify(err)}`); 292 | this.emit(":tell", this.t("UNKNOWN_SALESFORCE_ERROR")); 293 | } 294 | } 295 | 296 | /* 297 | * Update an opportunity attribute then shows the opportunity back to the user. 298 | */ 299 | const updateOpportunity = function (err, resp) { 300 | console.log("DEBUG - updateOpportunity callback function"); 301 | if (!err) { 302 | const opportunityId = this.attributes[constants.ATTRIBUTES_OPPORTUNITY_ID]; 303 | const accessToken = this.event.session.user.accessToken; 304 | sf.query(`Select id, Name, StageName, CloseDate, Amount From Opportunity Where id='${opportunityId}' LIMIT 1`, accessToken, renderUpdatedOpportunity, this); 305 | } else { 306 | console.error(`err: ${JSON.stringify(err)}`); 307 | this.emit(":tell", this.t("UNKNOWN_ERROR")); 308 | } 309 | } 310 | 311 | /* 312 | * Create the display card after updating an opportunity 313 | */ 314 | const renderUpdatedOpportunity = function (err, resp) { 315 | console.log(`renderUpdatedOpportunity. response: ${JSON.stringify(resp)}`); 316 | if (!err) { 317 | if (resp.records.length > 0) { 318 | const opp = resp.records[0]._fields; 319 | this.response.speak(this.t("OPPORTUNITY_UPDATE")) 320 | .cardRenderer(this.t("OPPORTUNITY_CARD_TITLE"), this.t("OPPORTUNITY_CARD", opp.id, opp.name, opp.closedate, opp.stagename, renderDollarAmount(opp.amount)), imageObj) 321 | .listen(this.t("LIKE_TO_DO")); 322 | this.emit(':responseReady'); 323 | } else { 324 | this.emit(":tell", this.t("OPPORTUNITY_ERROR")); 325 | } 326 | } else { 327 | console.error(`err: ${JSON.stringify(err)}`); 328 | this.speak(":tell", this.t("UNKNOWN_ERROR")); 329 | } 330 | } 331 | 332 | /* 333 | * Helper function to format dollar amounts 334 | */ 335 | const renderDollarAmount = function (amount) { 336 | let formatter = new Intl.NumberFormat("en-US", { 337 | style: "currency", 338 | currency: "USD", 339 | minimumFractionDigits: 0, 340 | }); 341 | 342 | return formatter.format(amount); 343 | } -------------------------------------------------------------------------------- /lambda/custom/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | Description: An AWS Serverless Specification template to deploy a private Alexa for Business skill with Salesforce integration. 4 | Parameters: 5 | SalesforceInstanceUrl: 6 | Description: 'The service URL used to make Salesforce queries. If using My Domain, looks like this: https://.my.salesforce.com. If not using My Domains, looks like this: https://na50.salesforce.com/.' 7 | Type: String 8 | SkillID: 9 | Description: 'The Alexa Skill ID to restrict access to this Lambda. Note: this can be provided in the Lambda UI via function triggers too.' 10 | Type: String 11 | Default: '' 12 | VoiceCodeObjectName: 13 | Description: The name used to create the Voice Code object in Salesforce. Default is 'voice_code__c'. 14 | Type: String 15 | Default: 'voice_code__c' 16 | VoiceCodeFieldName: 17 | Description: The name used to create the Code field inside of the Voice Code object. Default is 'code__c'. 18 | Type: String 19 | Default: 'code__c' 20 | Resources: 21 | AlexaSalesforceFunction: 22 | Type: 'AWS::Serverless::Function' 23 | Properties: 24 | Handler: index.handler 25 | Runtime: nodejs6.10 26 | CodeUri: . 27 | Description: 'A Lambda function that responds to Alexa requests and integrates with Salesforce. Created from https://github.com/alexa/alexa-for-business/tree/master/salesforce/skill-sample-nodejs-salesforce.' 28 | Environment: 29 | Variables: 30 | INSTANCE_URL: !Ref SalesforceInstanceUrl 31 | SKILL_ID: !Ref SkillID 32 | VOICE_CODE_OBJECT_NAME: !Ref VoiceCodeObjectName 33 | VOICE_CODE_FIELD_NAME: !Ref VoiceCodeFieldName 34 | Policies: 35 | - DynamoDBCrudPolicy: 36 | TableName: Salesforce_Skill 37 | MemorySize: 128 38 | Timeout: 3 39 | Events: 40 | AlexaSkillEvent: 41 | Type: AlexaSkill 42 | DynamoTable: 43 | Type: 'AWS::DynamoDB::Table' 44 | Properties: 45 | TableName: Salesforce_Skill 46 | AttributeDefinitions: 47 | - AttributeName: userId 48 | AttributeType: S 49 | KeySchema: 50 | - AttributeName: userId 51 | KeyType: HASH 52 | ProvisionedThroughput: 53 | ReadCapacityUnits: 5 54 | WriteCapacityUnits: 5 55 | -------------------------------------------------------------------------------- /lambda/custom/voiceCodeHandlers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | const Alexa = require('ask-sdk-v1adapter'); 21 | const bcrypt = require('bcryptjs'); 22 | 23 | const constants = require('./constants'); 24 | const sf = require('./salesforce'); 25 | 26 | const voiceCodeHandlers = { 27 | newSessionHandlers: Alexa.CreateStateHandler(constants.STATES.START, { 28 | 'LaunchRequest': function () { 29 | if (Object.keys(this.attributes).length === 0) { 30 | resetAttributes(true, this.attributes); 31 | } 32 | this.handler.state = constants.STATES.CODE; 33 | this.emitWithState("SetupAccount"); 34 | }, 35 | 'AMAZON.StartOverIntent': function () { 36 | resetAttributes(true, this.attributes); 37 | this.handler.state = constants.STATES.CODE; 38 | this.emitWithState("SetupAccount"); 39 | }, 40 | 'AMAZON.HelpIntent': function () { 41 | this.handler.state = constants.STATES.HELP; 42 | this.emitWithState("helpTheUser"); 43 | }, 44 | "Unhandled": function () { 45 | if (Object.keys(this.attributes).length === 0) { 46 | resetAttributes(true, this.attributes); 47 | } 48 | this.handler.state = constants.STATES.CODE; 49 | this.emitWithState("SetupAccount"); 50 | } 51 | }), 52 | 53 | codeStateHandlers: Alexa.CreateStateHandler(constants.STATES.CODE, { 54 | "SetupAccount": function () { 55 | // Access token is passed through the session information 56 | const accessToken = this.event.session.user.accessToken; 57 | let speechOutput; 58 | let repromptText; 59 | 60 | // Check to see if the user has linked their account 61 | if (accessToken) { 62 | const salesforceUserId = this.attributes[constants.SALESFORCE_USER_ID]; 63 | const hasCode = this.attributes[constants.ATTRIBUTES_HAS_CODE]; 64 | if (salesforceUserId && salesforceUserId.value && hasCode && hasCode.value && verifyVoiceCodeTimeout.call(this)) { 65 | // We know the salesforce user ID and that the person has a code 66 | speechOutput = this.t("WELCOME_MESSAGE") + this.t("WELCOME_HAS_CODE"); 67 | this.attributes[constants.ATTRIBUTES_HAS_CODE] = true; 68 | this.emit(":ask", speechOutput, this.t("WELCOME_HAS_CODE")); 69 | } else { 70 | // Get the user's ID 71 | sf.getIdentity(accessToken, identityCallback, this); 72 | } 73 | } else { 74 | // No account is linked. 75 | speechOutput = this.t("ACCOUNT_REQUIRED_MESSAGE"); 76 | this.emit(':tellWithLinkAccountCard', speechOutput); 77 | } 78 | }, 79 | "PromptForCode": function () { 80 | const firstAttempt = this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] === 0; 81 | const speechOutput = firstAttempt ? this.t("CODE_REQUEST") : this.t("CODE_REPEAT_REQUEST"); 82 | 83 | this.emit(":ask", speechOutput, speechOutput); 84 | }, 85 | "CodeIntent": function () { 86 | validateCode.call(this); 87 | }, 88 | "AMAZON.StartOverIntent": function () { 89 | resetAttributes(true, this.attributes); 90 | this.handler.state = constants.STATES.CODE; 91 | this.emitWithState("SetupAccount"); 92 | }, 93 | "AMAZON.HelpIntent": function () { 94 | this.handler.state = constants.STATES.HELP; 95 | this.emitWithState("helpTheUser"); 96 | }, 97 | "AMAZON.StopIntent": function () { 98 | resetAttributes(false, this.attributes); 99 | this.emit(":tell", this.t("STOP_MESSAGE")); 100 | }, 101 | "AMAZON.CancelIntent": function () { 102 | resetAttributes(false, this.attributes); 103 | this.emit(":tell", this.t("CANCEL_MESSAGE")); 104 | }, 105 | "Unhandled": function () { 106 | console.log(`DEBUG - in CODE unhandled, current attributes:\n${JSON.stringify(this.attributes)}`); 107 | this.emitWithState("SetupAccount"); 108 | }, 109 | "SessionEndedRequest": function () { 110 | console.log("Session ended waiting for a code: " + this.event.request.reason); 111 | }, 112 | "UnhandledError": function () { 113 | console.log("In constants.STATES.CODE UnhandledError"); 114 | 115 | if (Object.keys(this.attributes).length === 0) { 116 | resetAttributes(true, this.attributes); 117 | } 118 | this.emit(":tell", this.t("UNKNOWN_ERROR")); 119 | } 120 | }), 121 | 122 | changeCodeHandlers: Alexa.CreateStateHandler(constants.STATES.CHANGE_CODE, { 123 | "PromptForCode": function () { 124 | const firstAttempt = this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] === 0; 125 | const speechOutput = firstAttempt ? this.t("CHANGE_PROVIDE_CODE") : this.t("CODE_REPEAT_REQUEST"); 126 | 127 | this.emit(":ask", speechOutput, speechOutput); 128 | }, 129 | "CodeIntent": function () { 130 | validateCode.call(this); 131 | }, 132 | "AMAZON.StartOverIntent": function () { 133 | resetAttributes(true, this.attributes); 134 | this.handler.state = constants.STATES.CODE; 135 | this.emitWithState("SetupAccount"); 136 | }, 137 | "AMAZON.HelpIntent": function () { 138 | this.handler.state = constants.STATES.HELP; 139 | this.emitWithState("helpTheUser"); 140 | }, 141 | "AMAZON.StopIntent": function () { 142 | this.handler.state = constants.STATES.CODE; 143 | this.emitWithState("AMAZON.StopIntent"); 144 | }, 145 | "AMAZON.CancelIntent": function () { 146 | this.handler.state = constants.STATES.CODE; 147 | this.emitWithState("AMAZON.CancelIntent"); 148 | }, 149 | "Unhandled": function () { 150 | console.log("in Change Code unhandled"); 151 | this.handler.state = constants.STATES.CODE; 152 | this.emitWithState("UnhandledError"); 153 | }, 154 | "SessionEndedRequest": function () { 155 | console.log("Session ended in CHANGE_CODE state: " + this.event.request.reason); 156 | } 157 | }), 158 | 159 | newCodeHandlers: Alexa.CreateStateHandler(constants.STATES.NEW_CODE, { 160 | "PromptForCode": function () { 161 | const firstAttempt = this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] === 0; 162 | const speechOutput = firstAttempt ? this.t("CHANGE_NEW_CODE") : this.t("CODE_REPEAT_REQUEST"); 163 | 164 | this.emit(":ask", speechOutput, speechOutput); 165 | }, 166 | "CodeIntent": function () { 167 | validateCode.call(this); 168 | }, 169 | "AMAZON.StartOverIntent": function () { 170 | resetAttributes(true, this.attributes); 171 | this.handler.state = constants.STATES.CODE; 172 | this.emitWithState("SetupAccount"); 173 | }, 174 | "AMAZON.HelpIntent": function () { 175 | this.handler.state = constants.STATES.HELP; 176 | this.emitWithState("helpTheUser"); 177 | }, 178 | "AMAZON.StopIntent": function () { 179 | this.handler.state = constants.STATES.CODE; 180 | this.emitWithState("AMAZON.StopIntent"); 181 | }, 182 | "AMAZON.CancelIntent": function () { 183 | this.handler.state = constants.STATES.CODE; 184 | this.emitWithState("AMAZON.CancelIntent"); 185 | }, 186 | "Unhandled": function () { 187 | console.log("in New Code unhandled"); 188 | this.handler.state = constants.STATES.CODE; 189 | this.emitWithState("UnhandledError"); 190 | }, 191 | "SessionEndedRequest": function () { 192 | console.log("Session ended in NEW_CODE state: " + this.event.request.reason); 193 | } 194 | }), 195 | helpStateHandlers: Alexa.CreateStateHandler(constants.STATES.HELP, { 196 | "helpTheUser": function () { 197 | const speechOutput = this.t("HELP_MESSAGE"); 198 | const repromptText = this.t("SHORT_HELP"); 199 | // Defaulting to secure state for the next question, if the customer 200 | // doesn't have their code set, it will redirect to the code prompt process 201 | this.handler.state = constants.STATES.SECURE; 202 | this.emit(":ask", speechOutput, repromptText); 203 | }, 204 | "AMAZON.StartOverIntent": function () { 205 | resetAttributes(true, this.attributes); 206 | this.handler.state = constants.STATES.CODE; 207 | this.emitWithState("SetupAccount"); 208 | }, 209 | "AMAZON.HelpIntent": function () { 210 | this.emitWithState("helpTheUser"); 211 | }, 212 | "AMAZON.StopIntent": function () { 213 | this.handler.state = constants.STATES.CODE; 214 | this.emitWithState("AMAZON.StopIntent"); 215 | }, 216 | "AMAZON.CancelIntent": function () { 217 | this.handler.state = constants.STATES.CODE; 218 | this.emitWithState("AMAZON.CancelIntent"); 219 | }, 220 | "Unhandled": function () { 221 | console.log("in Help unhandled"); 222 | this.handler.state = constants.STATES.CODE; 223 | this.emitWithState("UnhandledError"); 224 | }, 225 | "SessionEndedRequest": function () { 226 | console.log("Session ended in help state: " + this.event.request.reason); 227 | } 228 | }) 229 | } 230 | 231 | /** 232 | * Callback function to handle getIdentity 233 | */ 234 | function identityCallback(err, resp) { 235 | let speechOutput = ""; 236 | if (!err) { 237 | // get Salesforce User ID 238 | const splitString = resp.identity.split('/'); 239 | const userId = splitString[splitString.length - 1] 240 | 241 | this.attributes[constants.SALESFORCE_USER_ID] = userId; 242 | const accessToken = this.event.session.user.accessToken; 243 | 244 | // Check to see if the user has a voice code or not 245 | sf.getVoiceCode(userId, accessToken, getVoiceCodeCallback, this); 246 | } else { 247 | console.log("Error in getIdentity call: " + JSON.stringify(err)); 248 | if (err.errorCode == "INVALID_SESSION_ID") { 249 | console.log("invalid session ID, prompt to relink"); 250 | speechOutput = this.t("ACCOUNT_RELINK_MESSAGE") 251 | this.emit(":tellWithLinkAccountCard", speechOutput); 252 | } else { 253 | console.log("Other unknown error during getIdentity call") 254 | speechOutput = this.t("UNKNOWN_SALESFORCE_ERROR"); 255 | this.emit(":tell", speechOutput); 256 | } 257 | } 258 | } 259 | 260 | /** 261 | * Callback function to handle getVoiceCode 262 | */ 263 | function getVoiceCodeCallback(err, resp) { 264 | let speechOutput = ""; 265 | if (!err) { 266 | if (resp.records.length > 0) { 267 | // User has a code, prompt them to say it. 268 | speechOutput = this.t("WELCOME_MESSAGE") + this.t("WELCOME_HAS_CODE"); 269 | this.attributes[constants.ATTRIBUTES_HAS_CODE] = true; 270 | this.emit(":ask", speechOutput, speechOutput); 271 | } else { 272 | // User doesn't have a code, prompt them to create one 273 | speechOutput = this.t("WELCOME_MESSAGE") + this.t("WELCOME_NO_CODE"); 274 | this.attributes[constants.ATTRIBUTES_HAS_CODE] = false; 275 | this.emit(":ask", speechOutput, speechOutput); 276 | } 277 | } else { 278 | console.log("Error in voice code query: " + JSON.stringify(err)); 279 | this.emit(":tell", this.t("UNKNOWN_SALESFORCE_ERROR")); 280 | } 281 | } 282 | 283 | /** 284 | * Helper function to reset session attributes to make code request begin again. 285 | */ 286 | function resetAttributes(resetTime, attributes) { 287 | if (resetTime) { 288 | attributes[constants.ATTRIBUTES_LAST_REQUEST] = null; 289 | } 290 | attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] = 0; 291 | attributes[constants.ATTRIBUTES_HAS_CODE] = null; 292 | attributes[constants.ATTRIBUTES_CHANGED_CODE] = null; 293 | attributes[constants.ATTRIBUTES_CREATED_CODE] = null; 294 | } 295 | 296 | /** 297 | * Helper function to tell if the voice code was last confirmed within an acceptable time range - default here is 5 minutes. 298 | * If the code is not valid, kicks back out to the state to prompt for code. 299 | * Requires current request context to access session attributes. 300 | */ 301 | const verifyVoiceCodeTimeout = function () { 302 | const timeSinceCodeRequest = (Date.now() - this.attributes[constants.ATTRIBUTES_LAST_REQUEST]) / 60000; 303 | if (timeSinceCodeRequest <= constants.CODE_TIMEOUT_MINUTES) { 304 | // If code is good, make sure numAttempts is zeroed out. 305 | this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] = 0; 306 | return true; 307 | } 308 | return false; 309 | } 310 | 311 | module.exports = voiceCodeHandlers; 312 | module.exports.resetAttributes = resetAttributes; 313 | module.exports.verifyVoiceCodeTimeout = verifyVoiceCodeTimeout; 314 | /** 315 | * Function to wrap checking the user's code and if it's valid, passing it on to see if it's correct. 316 | */ 317 | function validateCode() { 318 | const isCodeValid = isCodeSlotValid(this.event.request.intent); 319 | 320 | if (isCodeValid) { 321 | const code = parseInt(this.event.request.intent.slots.VOICE_CODE.value); 322 | 323 | if (this.handler.state == constants.STATES.NEW_CODE) { 324 | // Creating a new code 325 | setNewCode.call(this, code); 326 | } else { 327 | // Check for matching existing code 328 | const accessToken = this.event.session.user.accessToken; 329 | const salesforceUserId = this.attributes[constants.SALESFORCE_USER_ID]; 330 | const hasCode = this.attributes[constants.ATTRIBUTES_HAS_CODE]; 331 | 332 | // Check to see if the user has a voice code or not 333 | sf.getVoiceCode(salesforceUserId, accessToken, handleGetVoiceCode, this); 334 | } 335 | } else { 336 | // Invalid code entered 337 | this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] = parseInt(this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS]) + 1; 338 | //prompt user again for code 339 | this.emitWithState("PromptForCode"); 340 | } 341 | } 342 | 343 | /** 344 | * Callback function for getVoiceCode to determine if the code provided matches the user's set code. 345 | */ 346 | function handleGetVoiceCode(err, resp) { 347 | if (!err) { 348 | const code = parseInt(this.event.request.intent.slots.VOICE_CODE.value); 349 | 350 | if (resp.records.length > 0) { 351 | // User has a code, check the hashes 352 | const hashed_code = resp.records[0]._fields.code__c; 353 | 354 | // Check to see if the code provided matches the hash 355 | if (!bcrypt.compareSync(code.toString(), hashed_code)) { 356 | this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] = parseInt(this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS]) + 1; 357 | 358 | this.emitWithState("PromptForCode"); 359 | } else { 360 | this.attributes[constants.ATTRIBUTES_LAST_REQUEST] = Date.now(); 361 | this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] = 0; 362 | let speechOutput = ""; 363 | // If we're just authenticating, move to secure state. 364 | // If we're in change code state, move on to get the new code 365 | switch (this.handler.state) { 366 | case constants.STATES.CODE: 367 | this.handler.state = constants.STATES.SECURE; 368 | this.attributes[constants.ATTRIBUTES_CONFIRMED_CODE] = true; 369 | this.emitWithState("LaunchRequest") 370 | break; 371 | case constants.STATES.CHANGE_CODE: 372 | this.handler.state = constants.STATES.NEW_CODE; 373 | speechOutput = this.t("CHANGE_NEW_CODE"); 374 | this.emit(":ask", speechOutput, speechOutput); 375 | break; 376 | } 377 | } 378 | } else { 379 | // User doesn't have a code, set it 380 | setNewCode.call(this, code); 381 | } 382 | } else { 383 | console.log("Error in voice code query: " + JSON.stringify(err)); 384 | this.emit(":tell", this.t("UNKNOWN_SALESFORCE_ERROR")); 385 | } 386 | } 387 | 388 | /** 389 | * Creats a bcrypt hash of the code and saves it to Salesforce. 390 | */ 391 | function setNewCode(code) { 392 | const accessToken = this.event.session.user.accessToken; 393 | const salesforceUserId = this.attributes[constants.SALESFORCE_USER_ID]; 394 | 395 | // Hash the code - you can choose a different # of rounds, 10 is default 396 | // See https://www.npmjs.com/package/bcryptjs for more details. 397 | const salt = bcrypt.genSaltSync(10); 398 | const hash = bcrypt.hashSync(code.toString(), salt); 399 | 400 | // Add the code to Salesforce 401 | if (this.handler.state == constants.STATES.CODE) { 402 | sf.createVoiceCode(hash, salesforceUserId, accessToken, handleCreateCode, this); 403 | } else if (this.handler.state == constants.STATES.NEW_CODE) { 404 | sf.updateVoiceCode(hash, salesforceUserId, accessToken, handleUpdateCode, this); 405 | } 406 | } 407 | 408 | /** 409 | * Callback function used to create a voice code 410 | */ 411 | function handleCreateCode(err, resp) { 412 | if (!err) { 413 | this.handler.state = constants.STATES.SECURE; 414 | this.attributes[constants.ATTRIBUTES_LAST_REQUEST] = Date.now(); 415 | this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] = 0; 416 | this.attributes[constants.ATTRIBUTES_CREATED_CODE] = true; 417 | this.emitWithState("LaunchRequest"); 418 | } else { 419 | console.log('Error in insert custom setting method - ' + JSON.stringify(err)); 420 | this.emit(":tell", this.t("UNKNOWN_SALESFORCE_ERROR")); 421 | } 422 | } 423 | 424 | /** 425 | * Callback function used to update a voice code 426 | */ 427 | 428 | function handleUpdateCode(err, resp) { 429 | if (!err) { 430 | this.handler.state = constants.STATES.SECURE; 431 | this.attributes[constants.ATTRIBUTES_LAST_REQUEST] = Date.now(); 432 | this.attributes[constants.ATTRIBUTES_NUM_ATTEMPTS] = 0; 433 | this.attributes[constants.ATTRIBUTES_CHANGED_CODE] = true; 434 | this.emitWithState("LaunchRequest"); 435 | } else { 436 | console.log('Error in update custom setting method - ' + JSON.stringify(err)); 437 | this.emit(":tell", this.t("UNKNOWN_SALESFORCE_ERROR")); 438 | } 439 | } 440 | 441 | /** 442 | * Validates that the code provided to the skill is a number between 0000 and 9999. 443 | * @return {Boolean} if the code was a 4-digit number. 444 | */ 445 | function isCodeSlotValid(intent) { 446 | // Check to see if the intent has a code slot, the code slot is filled, the code slot is a number, and the number is between 0 and 9999 (a four digit number) 447 | const codeSlotFilled = intent && intent.slots && intent.slots.VOICE_CODE && intent.slots.VOICE_CODE.value; 448 | const codeSlotIsInt = codeSlotFilled && !isNaN(parseInt(intent.slots.VOICE_CODE.value)); 449 | return codeSlotIsInt && parseInt(intent.slots.VOICE_CODE.value) <= (9999) && parseInt(intent.slots.VOICE_CODE.value) >= 0; 450 | } -------------------------------------------------------------------------------- /skill-package/interactionModels/custom/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "salesforce demo", 5 | "intents": [ 6 | { 7 | "name": "AMAZON.StartOverIntent", 8 | "samples": [ 9 | "start over", 10 | "redo", 11 | "clear my settings" 12 | ] 13 | }, 14 | { 15 | "name": "ChangeCode", 16 | "slots": [], 17 | "samples": [ 18 | "I want to change my code", 19 | "change my code", 20 | "change my voice code", 21 | "I want to change my voice code" 22 | ] 23 | }, 24 | { 25 | "name": "CodeIntent", 26 | "slots": [ 27 | { 28 | "name": "VOICE_CODE", 29 | "type": "AMAZON.FOUR_DIGIT_NUMBER" 30 | } 31 | ], 32 | "samples": [ 33 | "the pin is {VOICE_CODE}", 34 | "my pin is {VOICE_CODE}", 35 | "{VOICE_CODE}" 36 | ] 37 | }, 38 | { 39 | "name": "RecentLead", 40 | "slots": [], 41 | "samples": [ 42 | "for my leads", 43 | "lead", 44 | "for a lead", 45 | "for a recent lead" 46 | ] 47 | }, 48 | { 49 | "name": "RecentOpportunity", 50 | "slots": [], 51 | "samples": [ 52 | "for my opportunities", 53 | "opportunity", 54 | "for an opportunity", 55 | "for a recent opportunity" 56 | ] 57 | }, 58 | { 59 | "name": "AMAZON.CancelIntent", 60 | "samples": [] 61 | }, 62 | { 63 | "name": "AMAZON.HelpIntent", 64 | "samples": [] 65 | }, 66 | { 67 | "name": "AMAZON.StopIntent", 68 | "samples": [] 69 | }, 70 | { 71 | "name": "SelectOpportunityIntent", 72 | "slots": [ 73 | { 74 | "name": "opportunity_name", 75 | "type": "AMAZON.SearchQuery" 76 | } 77 | ], 78 | "samples": [ 79 | "Select opportunity {opportunity_name}", 80 | "Select the {opportunity_name} opportunity", 81 | "open the {opportunity_name} opportunity", 82 | "open opportunity {opportunity_name}", 83 | "find opportunity {opportunity_name}", 84 | "find my opportunity {opportunity_name}", 85 | "find the {opportunity_name} opportunity ", 86 | "work on opportunity {opportunity_name}", 87 | "opportunity {opportunity_name}", 88 | "pick {opportunity_name}" 89 | ] 90 | }, 91 | { 92 | "name": "UpdateOpportunityAmountIntent", 93 | "slots": [ 94 | { 95 | "name": "opportunity_value", 96 | "type": "AMAZON.NUMBER", 97 | "samples": [ 98 | "The amount is {opportunity_value}", 99 | "{opportunity_value} dollars", 100 | "{opportunity_value}" 101 | ] 102 | } 103 | ], 104 | "samples": [ 105 | "update the opportunity amount to {opportunity_value}", 106 | "update the opportunity size to {opportunity_value}", 107 | "update the opportunity value to {opportunity_value}", 108 | "set the opportunity value to {opportunity_value}", 109 | "set value to {opportunity_value}", 110 | "update amount to {opportunity_value}", 111 | "change amount to {opportunity_value}", 112 | "set amount to {opportunity_value}", 113 | "amount of opportunity is {opportunity_value}", 114 | "opportunity amount is {opportunity_value}", 115 | "value {opportunity_value}", 116 | "amount {opportunity_value}", 117 | "change opportunity amount to {opportunity_value}" 118 | ] 119 | }, 120 | { 121 | "name": "UpdateOpportunityDateIntent", 122 | "slots": [ 123 | { 124 | "name": "opportunity_date", 125 | "type": "AMAZON.DATE", 126 | "samples": [ 127 | "the date is {opportunity_date}", 128 | "{opportunity_date}" 129 | ] 130 | } 131 | ], 132 | "samples": [ 133 | "update the opportunity closing date to {opportunity_date}", 134 | "update the opportunity close date to {opportunity_date}", 135 | "Set the opportunity close date to {opportunity_date}", 136 | "close date is {opportunity_date}", 137 | "date is {opportunity_date}", 138 | "set closing date to {opportunity_date}", 139 | "set close date to {opportunity_date}", 140 | "change close date to {opportunity_date}", 141 | "change date to {opportunity_date}" 142 | ] 143 | }, 144 | { 145 | "name": "UpdateOpportunityStageIntent", 146 | "slots": [ 147 | { 148 | "name": "opportunity_stage", 149 | "type": "OpportunityStage", 150 | "samples": [ 151 | "the stage is {opportunity_stage}", 152 | "{opportunity_stage} stage", 153 | "{opportunity_stage}" 154 | ] 155 | } 156 | ], 157 | "samples": [ 158 | "set the opportunity stage to {opportunity_stage}", 159 | "update the opportunity stage to {opportunity_stage}", 160 | "update the stage to {opportunity_stage}", 161 | "update stage to {opportunity_stage}", 162 | "set stage to {opportunity_stage}", 163 | "stage is {opportunity_stage}", 164 | "change stage to {opportunity_stage}" 165 | ] 166 | } 167 | ], 168 | "types": [ 169 | { 170 | "name": "OpportunityStage", 171 | "values": [ 172 | { 173 | "name": { 174 | "value": "Closed lost" 175 | } 176 | }, 177 | { 178 | "name": { 179 | "value": "Closed Won", 180 | "synonyms": [ 181 | "won" 182 | ] 183 | } 184 | }, 185 | { 186 | "name": { 187 | "value": "Commit", 188 | "synonyms": [ 189 | "committed" 190 | ] 191 | } 192 | }, 193 | { 194 | "name": { 195 | "value": "Proposal/Price Quote", 196 | "synonyms": [ 197 | "proposal price quote", 198 | "proposal slash price quote", 199 | "price quote", 200 | "proposal " 201 | ] 202 | } 203 | }, 204 | { 205 | "name": { 206 | "value": "Perception Analysis" 207 | } 208 | }, 209 | { 210 | "name": { 211 | "value": "Id. Decision Makers", 212 | "synonyms": [ 213 | "i. d. decision makers" 214 | ] 215 | } 216 | }, 217 | { 218 | "name": { 219 | "value": "Value Proposition", 220 | "synonyms": [ 221 | "value prop" 222 | ] 223 | } 224 | }, 225 | { 226 | "name": { 227 | "value": "Needs Analysis" 228 | } 229 | }, 230 | { 231 | "name": { 232 | "value": "Qualification" 233 | } 234 | }, 235 | { 236 | "name": { 237 | "value": "Prospecting", 238 | "synonyms": [ 239 | "prospect" 240 | ] 241 | } 242 | } 243 | ] 244 | } 245 | ] 246 | }, 247 | "dialog": { 248 | "intents": [ 249 | { 250 | "name": "UpdateOpportunityAmountIntent", 251 | "confirmationRequired": false, 252 | "prompts": {}, 253 | "slots": [ 254 | { 255 | "name": "opportunity_value", 256 | "type": "AMAZON.NUMBER", 257 | "confirmationRequired": false, 258 | "elicitationRequired": true, 259 | "prompts": { 260 | "elicitation": "Elicit.Slot.353309771982.971146653110" 261 | } 262 | } 263 | ] 264 | }, 265 | { 266 | "name": "UpdateOpportunityDateIntent", 267 | "confirmationRequired": false, 268 | "prompts": {}, 269 | "slots": [ 270 | { 271 | "name": "opportunity_date", 272 | "type": "AMAZON.DATE", 273 | "confirmationRequired": false, 274 | "elicitationRequired": true, 275 | "prompts": { 276 | "elicitation": "Elicit.Slot.294060573857.1175160548971" 277 | } 278 | } 279 | ] 280 | }, 281 | { 282 | "name": "UpdateOpportunityStageIntent", 283 | "confirmationRequired": false, 284 | "prompts": {}, 285 | "slots": [ 286 | { 287 | "name": "opportunity_stage", 288 | "type": "OpportunityStage", 289 | "confirmationRequired": false, 290 | "elicitationRequired": true, 291 | "prompts": { 292 | "elicitation": "Elicit.Slot.910695345118.760626685771" 293 | } 294 | } 295 | ] 296 | } 297 | ] 298 | }, 299 | "prompts": [ 300 | { 301 | "id": "Elicit.Slot.294060573857.1175160548971", 302 | "variations": [ 303 | { 304 | "type": "PlainText", 305 | "value": "What is the new close date?" 306 | } 307 | ] 308 | }, 309 | { 310 | "id": "Elicit.Slot.353309771982.971146653110", 311 | "variations": [ 312 | { 313 | "type": "PlainText", 314 | "value": "What is the new value?" 315 | } 316 | ] 317 | }, 318 | { 319 | "id": "Elicit.Slot.910695345118.760626685771", 320 | "variations": [ 321 | { 322 | "type": "PlainText", 323 | "value": "What is the new stage?" 324 | } 325 | ] 326 | } 327 | ] 328 | } 329 | } -------------------------------------------------------------------------------- /skill-package/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "A demonstration using Alexa to integrate with Salesforce.", 7 | "examplePhrases": [ 8 | "Alexa, ask Salesforce Demo select opportunity United Oil Installations", 9 | "Alexa, ask Salesforce Demo for an opportunity", 10 | "Alexa, ask Salesforce Demo to change my voice code" 11 | ], 12 | "name": "Salesforce Demo", 13 | "description": "A demonstration of Alexa skills accessing Salesforce data.\n\nIn order to access your Salesforce data, you will need to set up a voice code for the first time you use this skill.\n\nAfterwards, when you request your data by saying \"Alexa, ask Salesforce Demo for a recent opportunity\", Alexa will have you validate your voice code first to help determine if it's you.\n\nYou can also change your voice code by requesting \"Alexa, ask Salesforce Demo to change my voice code\".\n\nAt anytime, you can stop the skill by saying \"Alexa, stop\".", 14 | "smallIconUri": "https://s3.amazonaws.com/alexa-salesforce-demo/briefcase_108.png", 15 | "largeIconUri": "https://s3.amazonaws.com/alexa-salesforce-demo/briefcase_512.png" 16 | } 17 | }, 18 | "isAvailableWorldwide": true, 19 | "testingInstructions": "Required a Salesforce account to be linked for testing.", 20 | "category": "BUSINESS_AND_FINANCE", 21 | "distributionMode": "PRIVATE", 22 | "distributionCountries": [] 23 | }, 24 | "apis": { 25 | "custom": {} 26 | }, 27 | "manifestVersion": "1.0", 28 | "permissions": [], 29 | "privacyAndCompliance": { 30 | "allowsPurchases": false, 31 | "isExportCompliant": true, 32 | "containsAds": false, 33 | "isChildDirected": false, 34 | "usesPersonalInfo": false 35 | } 36 | } 37 | } 38 | --------------------------------------------------------------------------------