diff --git a/README.md b/README.md index 1e02382..3bd1b60 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,22 @@ [![npm version](https://badge.fury.io/js/hubot-botframework.svg)](https://badge.fury.io/js/hubot-botframework) [![Build Status](https://travis-ci.org/Microsoft/BotFramework-Hubot.svg?branch=master)](https://travis-ci.org/Microsoft/BotFramework-Hubot) [![Coverage Status](https://coveralls.io/repos/github/Microsoft/BotFramework-Hubot/badge.svg?branch=master)](https://coveralls.io/github/Microsoft/BotFramework-Hubot?branch=master) # Installation -Install `hubot`. Make sure to `npm install --save hubot-botframework` to add this module. Run the command `./bin/hubot -a botframework` to run the bot from your local computer. +### Use hubot in Bot Framework Supported Channels +1. Install `hubot`. Make sure to `npm install --save hubot-botframework` to add this module. + +2. Create a Botframework Registration by completing the [Bot Registration Page](https://dev.botframework.com/bots/new). Store the created app id and app password for use later. + +3. Configure the required environment variables, and run the command `./bin/hubot -a botframework` to run the bot from your local computer. + +You can then interact with your hubot through any Bot Framework supported channel. + +### Additional Steps to Use Hubot in [Microsoft Teams](https://products.office.com/en-US/microsoft-teams/) + +4. Create a Microsoft Teams app package (.zip) to upload in Teams. We recommend using the manifest editor in [App Studio for Microsoft Teams](https://docs.microsoft.com/en-us/microsoftteams/platform/get-started/get-started-app-studio). Include the bot's app id and password in the bots section. + +5. In Microsoft Teams, navigate to the Store and select `Upload a custom app`. Select the zipped Teams App Package, and install the bot for personal and/or team use. + +You can then interact with hubot through a personal chat or by @mentioning the name of the uploaded custom app in a Team. In personal chats, the bot's name can be dropped from messages(`ping` or `hubot ping`). In Teams, @mention the bot and omit the bot's name from the command (`@myhubot ping`). # Global Variables You can configure the Hubot BotFramework adapter through environment variables. @@ -13,14 +28,58 @@ Required (obtained from the BotFramework portal): 2. `BOTBUILDER_APP_PASSWORD` - This is the secret for your bot. Optional: -1. `BOTBUILDER_ENDPOINT` - Sets a custom HTTP endpoint for your bot to receive messages on (defualt is `/api/messages`). +1. `BOTBUILDER_ENDPOINT` - Sets a custom HTTP endpoint for your bot to receive messages on (default is `/api/messages`). + +2. `HUBOT_TEAMS_ENABLE_AUTH` - When set to `true`, restricts sending commands to hubot to a specific set of users in Teams. Messages from all non-Teams channels are blocked. Authorization is disabled by default. + +3. `HUBOT_TEAMS_INITIAL_ADMINS` - Required if `HUBOT_TEAMS_ENABLE_AUTH` is true. A comma-separated list of user principal names ([UPNs](https://docs.microsoft.com/en-us/windows/desktop/ADSchema/a-userprincipalname)). The users on this list will be admins and able to send commands to hubot when the hubot is first run with authorization enabled. # Channel Specific Variables -## [Microsoft Teams](https://products.office.com/en-US/microsoft-teams/) +### [Microsoft Teams](https://products.office.com/en-US/microsoft-teams/) These variables will only take effect if a user communicates with your hubot through [Microsoft Teams](https://products.office.com/en-US/microsoft-teams/). Optional: 1. `HUBOT_OFFICE365_TENANT_FILTER` - Comma seperated list of Office365 tenant Ids that are allowed to communicate with your hubot. By default ALL Office365 tenants can communicate with your hubot if they sideload your application manifest. +# Optional Authorization for Microsoft Teams: + +**NOTE:** The UPNs used for authorization are stored in the hubot brain, so brain persistence affects the use of `HUBOT_TEAMS_INITIAL_ADMINS` as described below. + +Authorization restricts the users that can send commands to hubot to a defined set of Microsoft Teams users. Authorization is currently only supported for the Teams channel, so when enabled, messages from all other channels are blocked. To maximize back compatibility, authorization is disabled by default and must be enabled to be used. + +### Configuring authorization +Authorization is set up using the `HUBOT_TEAMS_ENABLE_AUTH` and `HUBOT_TEAMS_INITIAL_ADMINS` environment variables. + +* `HUBOT_TEAMS_ENABLE_AUTH` controls whether authorization is enabled or not. If the variable is not set, authorization is disabled. To enable authorization, set the environment variable to `true`. + +* `HUBOT_TEAMS_INITIAL_ADMINS` is required if authorization is enabled. This variable contains a comma-separated list of UPNs. When the hubot is run with authorization enabled for the first time, the users whose UPNs are listed will be admins and authorized to send commands to hubot. These UPNs are stored in the hubot brain. After running hubot with authorization enabled for the first time: + + - If your hubot brain is persistent, to change the list of authorized users, first delete the stored list of authorized users from your hubot's brain then change `HUBOT_TEAMS_INITIAL_ADMINS` to the new list. Also consider using the [hubot-msteams](https://github.com/jayongg/TeamsHubot) script package to dynamically control authorizations. + + - If your hubot brain isn't persistent, the `HUBOT_TEAMS_INITIAL_ADMINS` list will be used to set admins every time hubot is restarted. + +# Card-based Interactions for Microsoft Teams + +**Add screenshots (create an images folder to store them in)** + +Hubot is great, but hubot without needing to type in whole commands and with less typos is even better. Card-based interactions wrap hubot responses into cards and provide buttons on the card containing useful follow-up commands. To run a follow-up command, simply click the button with the command. If user input is needed, another card is shown with fields for input, and the rest of the command is constructed for you. + +Currently, card based interactions are supported for the [hubot-github](https://github.com/hydal/hubot-github) package. + +### Defining new card-based interactions + +Adding new card-based interactions has two steps: + +1. Add entries to HubotResponseCards located in `src/hubot-response-cards.coffee`. Each entry is from a regex to an array of follow up commands. + * The regex should map to the command that you want to generate a card for with wildcards for the hubot's name and regexes for each user input. See the `hubot-github` entries for examples. + * The follow up queries should match the key for the follow up command in HubotQueryParts. + +2. Add entries to HubotQueryParts located in `src/hubot-query-parts.coffee`. Each entry is from the command to two arrays containing the text and input parts of a command. These arrays are used to construct the query with any user inputs to send to hubot. + * textParts contains the text surrounding any user inputs, if a command has no user input, it contains one string in textParts. Note that the first entry of textParts starts with 'hubot' + * inputParts contains representations of each user input in a command, if any. The text is used to prompt the user for input. + A special syntax can used for inputs with finite choices to create a dropdown selector. In this case, a / is used followed by the choices separated by the word " or ". See the `hubot-github` entries for examples. + +Once these entries have been added, cards with follow up commands will be generated for the commands added to HubotResponseCards. For menu cards used to initiate card-based interactions for any command in a script library, use the [hubot-msteams](https://github.com/jayongg/TeamsHubot) library. + # Contributing -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comm +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments diff --git a/package-lock.json b/package-lock.json index 28922a5..655d265 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,124 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/async": { + "version": "2.0.49", + "resolved": "https://registry.npmjs.org/@types/async/-/async-2.0.49.tgz", + "integrity": "sha512-Benr3i5odUkvpFkOpzGqrltGdbSs+EVCkEBGXbuR7uT0VzhXKIkhem6PDzHdx5EonA+rfbB3QvP6aDOw5+zp5Q==" + }, + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/caseless": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", + "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==" + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/events": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" + }, + "@types/express": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz", + "integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz", + "integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==", + "requires": { + "@types/events": "*", + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/jsonwebtoken": { + "version": "7.2.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz", + "integrity": "sha512-XENN3YzEB8D6TiUww0O8SRznzy1v+77lH7UmuN54xq/IHIsyWjWOzZuFFTtoiRuaE782uAoRwBe/wwow+vQXZw==", + "requires": { + "@types/node": "*" + } + }, + "@types/mime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" + }, + "@types/node": { + "version": "9.6.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.23.tgz", + "integrity": "sha512-d2SJJpwkiPudEQ3+9ysANN2Nvz4QJKUPoe/WL5zyQzI0RaEeZWH5K5xjvUIGszTItHQpFPdH+u51f6G/LkS8Cg==" + }, + "@types/range-parser": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", + "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==" + }, + "@types/request": { + "version": "2.47.1", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.1.tgz", + "integrity": "sha512-TV3XLvDjQbIeVxJ1Z3oCTDk/KuYwwcNKVwz2YaT0F5u86Prgc4syDAp6P96rkTQQ4bIdh+VswQIC9zS6NjY7/g==", + "requires": { + "@types/caseless": "*", + "@types/form-data": "*", + "@types/node": "*", + "@types/tough-cookie": "*" + } + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/sprintf-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/sprintf-js/-/sprintf-js-1.1.0.tgz", + "integrity": "sha512-AQSq0X2Pj+2UbLIAxRmnj7Tll4btd1fj3hFf0XxB3/ZT7qjQ2WA3/JKtehu8UXEbkNCcysG6ZnYs58F1e/FboA==" + }, + "@types/tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ==" + }, + "@types/url-join": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@types/url-join/-/url-join-0.8.2.tgz", + "integrity": "sha1-EYHsvh2XtwNODqHjXmLobMJrQi0=" + }, "abbrev": { "version": "1.0.9", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/abbrev/-/abbrev-1.0.9.tgz", @@ -16,7 +134,7 @@ "integrity": "sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo=", "dev": true, "requires": { - "mime-types": "2.1.17", + "mime-types": "~2.1.6", "negotiator": "0.5.3" } }, @@ -25,8 +143,8 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/ajv/-/ajv-4.11.8.tgz", "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" } }, "align-text": { @@ -35,9 +153,9 @@ "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" } }, "amdefine": { @@ -48,15 +166,13 @@ }, "ansi-regex": { "version": "2.1.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "2.2.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "argparse": { "version": "1.0.9", @@ -64,7 +180,7 @@ "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", "dev": true, "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" }, "dependencies": { "sprintf-js": { @@ -77,7 +193,7 @@ }, "asap": { "version": "2.0.6", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/asap/-/asap-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, "asn1": { @@ -85,6 +201,43 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/asn1/-/asn1-0.2.3.tgz", "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, "assert-plus": { "version": "0.2.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/assert-plus/-/assert-plus-0.2.0.tgz", @@ -98,12 +251,12 @@ }, "async": { "version": "1.5.2", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/async/-/async-1.5.2.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" }, "asynckit": { "version": "0.4.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/asynckit/-/asynckit-0.4.0.tgz", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "aws-sign2": { @@ -122,6 +275,12 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, "base64-url": { "version": "1.2.1", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/base64-url/-/base64-url-1.2.1.tgz", @@ -130,7 +289,7 @@ }, "base64url": { "version": "2.0.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/base64url/-/base64url-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" }, "basic-auth": { @@ -157,9 +316,43 @@ "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", "optional": true, "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, + "bl": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", + "integrity": "sha1-/cqHGplxOqANGeO7ukHER4emU5g=", + "requires": { + "readable-stream": "~2.0.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + } + } + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, "body-parser": { "version": "1.13.3", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/body-parser/-/body-parser-1.13.3.tgz", @@ -167,15 +360,15 @@ "dev": true, "requires": { "bytes": "2.1.0", - "content-type": "1.0.4", - "debug": "2.2.0", - "depd": "1.0.1", - "http-errors": "1.3.1", + "content-type": "~1.0.1", + "debug": "~2.2.0", + "depd": "~1.0.1", + "http-errors": "~1.3.1", "iconv-lite": "0.4.11", - "on-finished": "2.3.0", + "on-finished": "~2.3.0", "qs": "4.0.0", - "raw-body": "2.1.7", - "type-is": "1.6.15" + "raw-body": "~2.1.2", + "type-is": "~1.6.6" }, "dependencies": { "ee-first": { @@ -206,17 +399,17 @@ "dev": true, "requires": { "media-typer": "0.3.0", - "mime-types": "2.1.17" + "mime-types": "~2.1.15" } } } }, "boom": { "version": "2.10.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/boom/-/boom-2.10.1.tgz", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", "requires": { - "hoek": "2.16.3" + "hoek": "2.x.x" } }, "botbuilder": { @@ -224,15 +417,52 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/botbuilder/-/botbuilder-3.9.1.tgz", "integrity": "sha1-EErt42IYXQqjIizbVF4504jQnwY=", "requires": { - "async": "1.5.2", - "base64url": "2.0.0", - "chrono-node": "1.3.5", - "jsonwebtoken": "7.4.3", - "promise": "7.3.1", - "request": "2.81.0", - "rsa-pem-from-mod-exp": "0.8.4", - "sprintf-js": "1.1.1", - "url-join": "1.1.0" + "async": "^1.5.2", + "base64url": "^2.0.0", + "chrono-node": "^1.1.3", + "jsonwebtoken": "^7.0.1", + "promise": "^7.1.1", + "request": "^2.69.0", + "rsa-pem-from-mod-exp": "^0.8.4", + "sprintf-js": "^1.0.3", + "url-join": "^1.1.0" + } + }, + "botbuilder-teams": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/botbuilder-teams/-/botbuilder-teams-0.2.1.tgz", + "integrity": "sha1-Q0QDMYhdHS94Z8I75RZpqAJdg4g=", + "requires": { + "botbuilder": "^3.11.0", + "crypto": "^1.0.1", + "ms-rest": "^1.15.7", + "sprintf-js": "^1.1.1" + }, + "dependencies": { + "botbuilder": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-3.15.0.tgz", + "integrity": "sha512-KH5GTskHvTSozRPLdn8EQtg34zc71VUo+wXYzcoafRCkGVqqxB4AitUqow8y8ENiHgJW3oY2Pvhd8glcZiraOA==", + "requires": { + "@types/async": "^2.0.48", + "@types/express": "^4.11.1", + "@types/form-data": "^2.2.1", + "@types/jsonwebtoken": "^7.2.6", + "@types/node": "^9.6.1", + "@types/request": "^2.47.0", + "@types/sprintf-js": "^1.1.0", + "@types/url-join": "^0.8.1", + "async": "^1.5.2", + "base64url": "^2.0.0", + "chrono-node": "^1.1.3", + "jsonwebtoken": "^7.0.1", + "promise": "^7.1.1", + "request": "^2.69.0", + "rsa-pem-from-mod-exp": "^0.8.4", + "sprintf-js": "^1.0.3", + "url-join": "^1.1.0" + } + } } }, "brace-expansion": { @@ -241,21 +471,137 @@ "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, "browser-stdout": { "version": "1.3.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/browser-stdout/-/browser-stdout-1.3.0.tgz", "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", "dev": true }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, "buffer-equal-constant-time": { "version": "1.0.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, "bytes": { "version": "2.1.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/bytes/-/bytes-2.1.0.tgz", @@ -271,7 +617,7 @@ }, "caseless": { "version": "0.12.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/caseless/-/caseless-0.12.0.tgz", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "center-align": { @@ -281,8 +627,8 @@ "dev": true, "optional": true, "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" } }, "chai": { @@ -291,25 +637,24 @@ "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", "dev": true, "requires": { - "assertion-error": "1.0.2", - "check-error": "1.0.2", - "deep-eql": "3.0.1", - "get-func-name": "2.0.0", - "pathval": "1.1.0", - "type-detect": "4.0.3" + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" } }, "chalk": { "version": "1.1.3", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" } }, "check-error": { @@ -320,10 +665,20 @@ }, "chrono-node": { "version": "1.3.5", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/chrono-node/-/chrono-node-1.3.5.tgz", + "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-1.3.5.tgz", "integrity": "sha1-oklSmKMtqCvMAa2b59d++l4kQSI=", "requires": { - "moment": "2.18.1" + "moment": "^2.10.3" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "cline": { @@ -339,8 +694,8 @@ "dev": true, "optional": true, "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", + "center-align": "^0.1.1", + "right-align": "^0.1.1", "wordwrap": "0.0.2" }, "dependencies": { @@ -355,7 +710,7 @@ }, "co": { "version": "4.6.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/co/-/co-4.6.0.tgz", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, "coffee-coverage": { @@ -364,11 +719,11 @@ "integrity": "sha1-eMkaayeG6FFIsVQMI2WTo9RZzVU=", "dev": true, "requires": { - "argparse": "1.0.9", - "coffee-script": "1.12.7", - "lodash": "4.17.4", - "minimatch": "3.0.4", - "pkginfo": "0.4.1" + "argparse": "^1.0.2", + "coffee-script": ">=1.6.2", + "lodash": "^4.14.0", + "minimatch": "^3.0.2", + "pkginfo": ">=0.2.3" } }, "coffee-script": { @@ -382,7 +737,7 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/combined-stream/-/combined-stream-1.0.5.tgz", "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "commander": { @@ -391,13 +746,19 @@ "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", "dev": true }, + "compare-module-exports": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/compare-module-exports/-/compare-module-exports-2.0.1.tgz", + "integrity": "sha512-kpkXUcxwce4IyRpw83pWz53SzOFIISV3kEP4fI/UIBh7M8Isom539h8LYb7YlRXIUA67DhTH5zTLq2qRkq9THA==", + "dev": true + }, "compressible": { "version": "2.0.11", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/compressible/-/compressible-2.0.11.tgz", "integrity": "sha1-FnGKdd4oPtjmBAQWJaIGRYZ5fYo=", "dev": true, "requires": { - "mime-db": "1.30.0" + "mime-db": ">= 1.29.0 < 2" } }, "compression": { @@ -406,12 +767,12 @@ "integrity": "sha1-sDuNhub4rSloPLqN+R3cb/x3s5U=", "dev": true, "requires": { - "accepts": "1.2.13", + "accepts": "~1.2.12", "bytes": "2.1.0", - "compressible": "2.0.11", - "debug": "2.2.0", - "on-headers": "1.0.1", - "vary": "1.0.1" + "compressible": "~2.0.5", + "debug": "~2.2.0", + "on-headers": "~1.0.0", + "vary": "~1.0.1" } }, "concat-map": { @@ -427,36 +788,36 @@ "dev": true, "requires": { "basic-auth-connect": "1.0.0", - "body-parser": "1.13.3", + "body-parser": "~1.13.3", "bytes": "2.1.0", - "compression": "1.5.2", - "connect-timeout": "1.6.2", - "content-type": "1.0.4", + "compression": "~1.5.2", + "connect-timeout": "~1.6.2", + "content-type": "~1.0.1", "cookie": "0.1.3", - "cookie-parser": "1.3.5", + "cookie-parser": "~1.3.5", "cookie-signature": "1.0.6", - "csurf": "1.8.3", - "debug": "2.2.0", - "depd": "1.0.1", - "errorhandler": "1.4.3", - "express-session": "1.11.3", + "csurf": "~1.8.3", + "debug": "~2.2.0", + "depd": "~1.0.1", + "errorhandler": "~1.4.2", + "express-session": "~1.11.3", "finalhandler": "0.4.0", "fresh": "0.3.0", - "http-errors": "1.3.1", - "method-override": "2.3.9", - "morgan": "1.6.1", + "http-errors": "~1.3.1", + "method-override": "~2.3.5", + "morgan": "~1.6.1", "multiparty": "3.3.2", - "on-headers": "1.0.1", - "parseurl": "1.3.2", + "on-headers": "~1.0.0", + "parseurl": "~1.3.0", "pause": "0.1.0", "qs": "4.0.0", - "response-time": "2.3.2", - "serve-favicon": "2.3.2", - "serve-index": "1.7.3", - "serve-static": "1.10.3", - "type-is": "1.6.15", + "response-time": "~2.3.1", + "serve-favicon": "~2.3.0", + "serve-index": "~1.7.2", + "serve-static": "~1.10.0", + "type-is": "~1.6.6", "utils-merge": "1.0.0", - "vhost": "3.0.2" + "vhost": "~3.0.1" }, "dependencies": { "qs": { @@ -472,7 +833,7 @@ "dev": true, "requires": { "media-typer": "0.3.0", - "mime-types": "2.1.17" + "mime-types": "~2.1.15" } } } @@ -483,10 +844,10 @@ "integrity": "sha1-L6vs/cGop3S6GUhNzmYMgYqFVec=", "dev": true, "requires": { - "multiparty": "3.3.2", - "on-finished": "2.1.1", - "qs": "2.2.5", - "type-is": "1.5.7" + "multiparty": "~3.3.2", + "on-finished": "~2.1.0", + "qs": "~2.2.4", + "type-is": "~1.5.2" }, "dependencies": { "qs": { @@ -503,10 +864,10 @@ "integrity": "sha1-3ppexh4zoStu2qt7XwYumMWZuI4=", "dev": true, "requires": { - "debug": "2.2.0", - "http-errors": "1.3.1", + "debug": "~2.2.0", + "http-errors": "~1.3.1", "ms": "0.7.1", - "on-headers": "1.0.1" + "on-headers": "~1.0.0" }, "dependencies": { "ms": { @@ -517,6 +878,21 @@ } } }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, "content-disposition": { "version": "0.5.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/content-disposition/-/content-disposition-0.5.0.tgz", @@ -553,7 +929,7 @@ }, "core-util-is": { "version": "1.0.2", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/core-util-is/-/core-util-is-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "coveralls": { @@ -587,10 +963,10 @@ "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", "dev": true, "requires": { - "chalk": "1.1.3", - "commander": "2.11.0", - "is-my-json-valid": "2.16.1", - "pinkie-promise": "2.0.1" + "chalk": "^1.1.1", + "commander": "^2.9.0", + "is-my-json-valid": "^2.12.4", + "pinkie-promise": "^2.0.0" } }, "js-yaml": { @@ -599,8 +975,8 @@ "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", "dev": true, "requires": { - "argparse": "1.0.9", - "esprima": "2.7.3" + "argparse": "^1.0.7", + "esprima": "^2.6.0" } }, "minimist": { @@ -621,26 +997,26 @@ "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", "dev": true, "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.11.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "2.0.6", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "qs": "6.3.2", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.4.3", - "uuid": "3.1.0" + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.11.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~2.0.6", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "qs": "~6.3.0", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "~0.4.1", + "uuid": "^3.0.0" } }, "tunnel-agent": { @@ -657,12 +1033,73 @@ "integrity": "sha1-+mIuG8OIvyVzCQgta2UgDOZwkLo=", "dev": true }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, "cryptiles": { "version": "2.0.5", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/cryptiles/-/cryptiles-2.0.5.tgz", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", "requires": { - "boom": "2.10.1" + "boom": "2.x.x" + } + }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" } }, "csrf": { @@ -684,16 +1121,16 @@ "requires": { "cookie": "0.1.3", "cookie-signature": "1.0.6", - "csrf": "3.0.6", - "http-errors": "1.3.1" + "csrf": "~3.0.0", + "http-errors": "~1.3.1" } }, "dashdash": { "version": "1.14.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/dashdash/-/dashdash-1.14.1.tgz", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" }, "dependencies": { "assert-plus": { @@ -703,6 +1140,12 @@ } } }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, "debug": { "version": "2.2.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/debug/-/debug-2.2.0.tgz", @@ -733,7 +1176,7 @@ "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, "requires": { - "type-detect": "4.0.3" + "type-detect": "^4.0.0" } }, "deep-is": { @@ -744,7 +1187,7 @@ }, "delayed-stream": { "version": "1.0.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/delayed-stream/-/delayed-stream-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "depd": { @@ -753,6 +1196,16 @@ "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo=", "dev": true }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "destroy": { "version": "1.0.4", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/destroy/-/destroy-1.0.4.tgz", @@ -765,13 +1218,35 @@ "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", "dev": true }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "ecdsa-sig-formatter": { @@ -779,8 +1254,8 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", "requires": { - "base64url": "2.0.0", - "safe-buffer": "5.1.1" + "base64url": "^2.0.0", + "safe-buffer": "^5.0.1" } }, "ee-first": { @@ -789,14 +1264,29 @@ "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=", "dev": true }, + "elliptic": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, "errorhandler": { "version": "1.4.3", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/errorhandler/-/errorhandler-1.4.3.tgz", "integrity": "sha1-t7cO2PNZ6duICS8tIMD4MUIK2D8=", "dev": true, "requires": { - "accepts": "1.3.4", - "escape-html": "1.0.3" + "accepts": "~1.3.0", + "escape-html": "~1.0.3" }, "dependencies": { "accepts": { @@ -805,7 +1295,7 @@ "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", "dev": true, "requires": { - "mime-types": "2.1.17", + "mime-types": "~2.1.16", "negotiator": "0.6.1" } }, @@ -831,9 +1321,8 @@ }, "escape-string-regexp": { "version": "1.0.5", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.8.1", @@ -841,11 +1330,11 @@ "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", "dev": true, "requires": { - "esprima": "2.7.3", - "estraverse": "1.9.3", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.2.0" + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" } }, "esprima": { @@ -872,33 +1361,49 @@ "integrity": "sha1-A9MLX2fdbmMtKUXTDWZScxo01dg=", "dev": true }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, "express": { "version": "3.21.2", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/express/-/express-3.21.2.tgz", "integrity": "sha1-DCkD7lxU5j1lqWFwdkcDVQZlo94=", "dev": true, "requires": { - "basic-auth": "1.0.4", + "basic-auth": "~1.0.3", "commander": "2.6.0", "connect": "2.30.2", "content-disposition": "0.5.0", - "content-type": "1.0.4", + "content-type": "~1.0.1", "cookie": "0.1.3", "cookie-signature": "1.0.6", - "debug": "2.2.0", - "depd": "1.0.1", + "debug": "~2.2.0", + "depd": "~1.0.1", "escape-html": "1.0.2", - "etag": "1.7.0", + "etag": "~1.7.0", "fresh": "0.3.0", "merge-descriptors": "1.0.0", - "methods": "1.1.2", + "methods": "~1.1.1", "mkdirp": "0.5.1", - "parseurl": "1.3.2", - "proxy-addr": "1.0.10", - "range-parser": "1.0.3", + "parseurl": "~1.3.0", + "proxy-addr": "~1.0.8", + "range-parser": "~1.0.2", "send": "0.13.0", "utils-merge": "1.0.0", - "vary": "1.0.1" + "vary": "~1.0.1" } }, "express-session": { @@ -910,11 +1415,11 @@ "cookie": "0.1.3", "cookie-signature": "1.0.6", "crc": "3.3.0", - "debug": "2.2.0", - "depd": "1.0.1", - "on-headers": "1.0.1", - "parseurl": "1.3.2", - "uid-safe": "2.0.0", + "debug": "~2.2.0", + "depd": "~1.0.1", + "on-headers": "~1.0.0", + "parseurl": "~1.3.0", + "uid-safe": "~2.0.0", "utils-merge": "1.0.0" }, "dependencies": { @@ -936,7 +1441,7 @@ }, "extsprintf": { "version": "1.3.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/extsprintf/-/extsprintf-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-levenshtein": { @@ -951,10 +1456,10 @@ "integrity": "sha1-llpS2ejQXSuFdUhUH7ibU6JJfZs=", "dev": true, "requires": { - "debug": "2.2.0", + "debug": "~2.2.0", "escape-html": "1.0.2", - "on-finished": "2.3.0", - "unpipe": "1.0.0" + "on-finished": "~2.3.0", + "unpipe": "~1.0.0" }, "dependencies": { "ee-first": { @@ -976,7 +1481,7 @@ }, "forever-agent": { "version": "0.6.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/forever-agent/-/forever-agent-0.6.1.tgz", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { @@ -984,9 +1489,9 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/form-data/-/form-data-2.1.4.tgz", "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" } }, "forwarded": { @@ -1009,17 +1514,15 @@ }, "generate-function": { "version": "2.0.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" }, "generate-object-property": { "version": "1.2.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/generate-object-property/-/generate-object-property-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, "requires": { - "is-property": "1.0.2" + "is-property": "^1.0.0" } }, "get-func-name": { @@ -1030,10 +1533,10 @@ }, "getpass": { "version": "0.1.7", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/getpass/-/getpass-0.1.7.tgz", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" }, "dependencies": { "assert-plus": { @@ -1049,11 +1552,11 @@ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", "dev": true, "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "graceful-readlink": { @@ -1074,10 +1577,10 @@ "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", "dev": true, "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" }, "dependencies": { "source-map": { @@ -1086,7 +1589,7 @@ "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, "requires": { - "amdefine": "1.0.1" + "amdefine": ">=0.0.4" } } } @@ -1101,17 +1604,16 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/har-validator/-/har-validator-4.2.1.tgz", "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" + "ajv": "^4.9.1", + "har-schema": "^1.0.5" } }, "has-ansi": { "version": "2.0.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/has-ansi/-/has-ansi-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "has-flag": { @@ -1120,15 +1622,35 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", + "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "hawk": { "version": "3.1.3", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/hawk/-/hawk-3.1.3.tgz", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" } }, "he": { @@ -1137,9 +1659,20 @@ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "hoek": { "version": "2.16.3", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/hoek/-/hoek-2.16.3.tgz", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" }, "http-errors": { @@ -1148,8 +1681,8 @@ "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", "dev": true, "requires": { - "inherits": "2.0.3", - "statuses": "1.3.1" + "inherits": "~2.0.1", + "statuses": "1" } }, "http-signature": { @@ -1157,23 +1690,29 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/http-signature/-/http-signature-1.1.1.tgz", "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, "hubot": { "version": "2.19.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/hubot/-/hubot-2.19.0.tgz", "integrity": "sha1-h8Vy0hD7DV+J91YXeuACDUn/ujY=", "dev": true, "requires": { - "async": "0.9.2", - "chalk": "1.1.3", - "cline": "0.8.2", + "async": ">=0.1.0 <1.0.0", + "chalk": "^1.0.0", + "cline": "^0.8.2", "coffee-script": "1.6.3", - "connect-multiparty": "1.2.5", - "express": "3.21.2", + "connect-multiparty": "^1.2.5", + "express": "^3.21.2", "log": "1.4.0", "optparse": "1.0.4", "scoped-http-client": "0.11.0" @@ -1199,21 +1738,32 @@ "integrity": "sha1-LstC/SlHRJIiCaLnxATayHk9it4=", "dev": true }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { "version": "2.0.3", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ipaddr.js": { "version": "1.0.5", @@ -1231,23 +1781,21 @@ "version": "2.16.1", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==", - "dev": true, "requires": { - "generate-function": "2.0.0", - "generate-object-property": "1.2.0", - "jsonpointer": "4.0.1", - "xtend": "4.0.1" + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" } }, "is-property": { "version": "1.0.2", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" }, "is-typedarray": { "version": "1.0.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/is-typedarray/-/is-typedarray-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "isarray": { @@ -1258,7 +1806,7 @@ }, "isemail": { "version": "1.2.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/isemail/-/isemail-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" }, "isexe": { @@ -1269,7 +1817,7 @@ }, "isstream": { "version": "0.1.2", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/isstream/-/isstream-0.1.2.tgz", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul": { @@ -1278,20 +1826,20 @@ "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", "dev": true, "requires": { - "abbrev": "1.0.9", - "async": "1.5.2", - "escodegen": "1.8.1", - "esprima": "2.7.3", - "glob": "5.0.15", - "handlebars": "4.0.10", - "js-yaml": "3.10.0", - "mkdirp": "0.5.1", - "nopt": "3.0.6", - "once": "1.4.0", - "resolve": "1.1.7", - "supports-color": "3.2.3", - "which": "1.3.0", - "wordwrap": "1.0.0" + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" }, "dependencies": { "supports-color": { @@ -1300,20 +1848,20 @@ "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", "dev": true, "requires": { - "has-flag": "1.0.0" + "has-flag": "^1.0.0" } } } }, "joi": { "version": "6.10.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/joi/-/joi-6.10.1.tgz", + "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", "requires": { - "hoek": "2.16.3", - "isemail": "1.2.0", - "moment": "2.18.1", - "topo": "1.1.0" + "hoek": "2.x.x", + "isemail": "1.x.x", + "moment": "2.x.x", + "topo": "1.x.x" } }, "js-yaml": { @@ -1322,8 +1870,8 @@ "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", "dev": true, "requires": { - "argparse": "1.0.9", - "esprima": "4.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, "dependencies": { "esprima": { @@ -1336,13 +1884,13 @@ }, "jsbn": { "version": "0.1.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/jsbn/-/jsbn-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "optional": true }, "json-schema": { "version": "0.2.3", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/json-schema/-/json-schema-0.2.3.tgz", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-stable-stringify": { @@ -1350,12 +1898,12 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", "requires": { - "jsonify": "0.0.0" + "jsonify": "~0.0.0" } }, "json-stringify-safe": { "version": "5.0.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json3": { @@ -1371,25 +1919,24 @@ }, "jsonpointer": { "version": "4.0.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" }, "jsonwebtoken": { "version": "7.4.3", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz", "integrity": "sha1-d/UCHeBYtgWheD+hKD6ZgS5kVjg=", "requires": { - "joi": "6.10.1", - "jws": "3.1.4", - "lodash.once": "4.1.1", - "ms": "2.0.0", - "xtend": "4.0.1" + "joi": "^6.10.1", + "jws": "^3.1.4", + "lodash.once": "^4.0.0", + "ms": "^2.0.0", + "xtend": "^4.0.1" } }, "jsprim": { "version": "1.4.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/jsprim/-/jsprim-1.4.1.tgz", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", "requires": { "assert-plus": "1.0.0", @@ -1413,7 +1960,7 @@ "base64url": "2.0.0", "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.9", - "safe-buffer": "5.1.1" + "safe-buffer": "^5.0.1" } }, "jws": { @@ -1421,9 +1968,9 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/jws/-/jws-3.1.4.tgz", "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", "requires": { - "base64url": "2.0.0", - "jwa": "1.1.5", - "safe-buffer": "5.1.1" + "base64url": "^2.0.0", + "jwa": "^1.1.4", + "safe-buffer": "^5.0.1" } }, "kind-of": { @@ -1432,7 +1979,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.5" + "is-buffer": "^1.1.5" } }, "lazy-cache": { @@ -1454,8 +2001,8 @@ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "dev": true, "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" } }, "lodash": { @@ -1470,8 +2017,8 @@ "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", "dev": true, "requires": { - "lodash._basecopy": "3.0.1", - "lodash.keys": "3.1.2" + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" } }, "lodash._basecopy": { @@ -1498,15 +2045,21 @@ "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", "dev": true }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, "lodash.create": { "version": "3.1.1", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/lodash.create/-/lodash.create-3.1.1.tgz", "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", "dev": true, "requires": { - "lodash._baseassign": "3.2.0", - "lodash._basecreate": "3.0.3", - "lodash._isiterateecall": "3.0.9" + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" } }, "lodash.isarguments": { @@ -1527,16 +2080,41 @@ "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", "dev": true, "requires": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" } }, "lodash.once": { "version": "4.1.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/lodash.once/-/lodash.once-4.1.1.tgz", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", + "dev": true + }, + "lodash.template": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", + "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~3.0.0" + } + }, "log": { "version": "1.4.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/log/-/log-1.4.0.tgz", @@ -1555,6 +2133,16 @@ "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", "dev": true }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/media-typer/-/media-typer-0.3.0.tgz", @@ -1574,9 +2162,9 @@ "dev": true, "requires": { "debug": "2.6.8", - "methods": "1.1.2", - "parseurl": "1.3.2", - "vary": "1.1.1" + "methods": "~1.1.2", + "parseurl": "~1.3.1", + "vary": "~1.1.1" }, "dependencies": { "debug": { @@ -1602,6 +2190,16 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", "dev": true }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, "mime": { "version": "1.3.4", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/mime/-/mime-1.3.4.tgz", @@ -1618,16 +2216,28 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/mime-types/-/mime-types-2.1.17.tgz", "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", "requires": { - "mime-db": "1.30.0" + "mime-db": "~1.30.0" } }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "brace-expansion": "1.1.8" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -1671,7 +2281,7 @@ "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", "dev": true, "requires": { - "graceful-readlink": "1.0.1" + "graceful-readlink": ">= 1.0.0" } }, "debug": { @@ -1689,12 +2299,12 @@ "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "supports-color": { @@ -1703,7 +2313,7 @@ "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", "dev": true, "requires": { - "has-flag": "1.0.0" + "has-flag": "^1.0.0" } } } @@ -1725,11 +2335,11 @@ "integrity": "sha1-X9gYOYxoGcuiinzWZk8pL+HAu/I=", "dev": true, "requires": { - "basic-auth": "1.0.4", - "debug": "2.2.0", - "depd": "1.0.1", - "on-finished": "2.3.0", - "on-headers": "1.0.1" + "basic-auth": "~1.0.3", + "debug": "~2.2.0", + "depd": "~1.0.1", + "on-finished": "~2.3.0", + "on-headers": "~1.0.0" }, "dependencies": { "ee-first": { @@ -1754,14 +2364,111 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "ms-rest": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/ms-rest/-/ms-rest-1.15.7.tgz", + "integrity": "sha1-QAUV4FsZJIicthoexgVCkKaOEgc=", + "requires": { + "duplexer": "~0.1.1", + "moment": "^2.14.1", + "request": "~2.74.0", + "through": "~2.3.4", + "tunnel": "~0.0.2", + "uuid": "^3.0.1" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "^4.17.10" + } + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + }, + "commander": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", + "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==" + }, + "form-data": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", + "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", + "requires": { + "async": "^2.0.1", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.11" + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "requires": { + "chalk": "^1.1.1", + "commander": "^2.9.0", + "is-my-json-valid": "^2.12.4", + "pinkie-promise": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "qs": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz", + "integrity": "sha1-HPyyXBCpsrSDBT/zn138kjOQjP4=" + }, + "request": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.74.0.tgz", + "integrity": "sha1-dpPKdou7DqXIzgjAhKRe+gW4kqs=", + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "bl": "~1.1.2", + "caseless": "~0.11.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~1.0.0-rc4", + "har-validator": "~2.0.6", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "node-uuid": "~1.4.7", + "oauth-sign": "~0.8.1", + "qs": "~6.2.0", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "~0.4.1" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" + } + } + }, "multiparty": { "version": "3.3.2", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/multiparty/-/multiparty-3.3.2.tgz", "integrity": "sha1-Nd5oBNwZZD5SSfPT473GyM4wHT8=", "dev": true, "requires": { - "readable-stream": "1.1.14", - "stream-counter": "0.2.0" + "readable-stream": "~1.1.9", + "stream-counter": "~0.2.0" } }, "negotiator": { @@ -1770,13 +2477,87 @@ "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g=", "dev": true }, + "node-libs-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", + "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^1.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.0", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + }, "nopt": { "version": "3.0.6", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, "requires": { - "abbrev": "1.0.9" + "abbrev": "1" } }, "oauth-sign": { @@ -1805,7 +2586,7 @@ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "optimist": { @@ -1814,8 +2595,8 @@ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "dev": true, "requires": { - "minimist": "0.0.8", - "wordwrap": "0.0.3" + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" }, "dependencies": { "wordwrap": { @@ -1832,12 +2613,12 @@ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", "dev": true, "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" } }, "optparse": { @@ -1846,23 +2627,60 @@ "integrity": "sha1-wGJXnS0F0kPCIaMEpx4Ml5YjzPE=", "dev": true }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, "parent-require": { "version": "1.0.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/parent-require/-/parent-require-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", "integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc=" }, + "parse-asn1": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3" + } + }, "parseurl": { "version": "1.3.2", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/parseurl/-/parseurl-1.3.2.tgz", "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", "dev": true }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, "pathval": { "version": "1.1.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/pathval/-/pathval-1.1.0.tgz", @@ -1875,6 +2693,19 @@ "integrity": "sha1-68ikqGGf8LioGsFRPDQ0/0af23Q=", "dev": true }, + "pbkdf2": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", + "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, "performance-now": { "version": "0.2.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/performance-now/-/performance-now-0.2.0.tgz", @@ -1882,17 +2713,15 @@ }, "pinkie": { "version": "2.0.4", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" }, "pinkie-promise": { "version": "2.0.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, "requires": { - "pinkie": "2.0.4" + "pinkie": "^2.0.0" } }, "pkginfo": { @@ -1907,12 +2736,23 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, "promise": { "version": "7.3.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/promise/-/promise-7.3.1.tgz", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", "requires": { - "asap": "2.0.6" + "asap": "~2.0.3" } }, "proxy-addr": { @@ -1921,13 +2761,26 @@ "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=", "dev": true, "requires": { - "forwarded": "0.1.1", + "forwarded": "~0.1.0", "ipaddr.js": "1.0.5" } }, + "public-encrypt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", + "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1" + } + }, "punycode": { "version": "1.4.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/punycode/-/punycode-1.4.1.tgz", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "qs": { @@ -1935,12 +2788,43 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/qs/-/qs-6.4.0.tgz", "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, "random-bytes": { "version": "1.0.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/random-bytes/-/random-bytes-1.0.0.tgz", "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=", "dev": true }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, "range-parser": { "version": "1.0.3", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/range-parser/-/range-parser-1.0.3.tgz", @@ -1978,10 +2862,10 @@ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "repeat-string": { @@ -1995,28 +2879,28 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/request/-/request-2.81.0.tgz", "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" } }, "resolve": { @@ -2031,8 +2915,8 @@ "integrity": "sha1-/6cbq5UtYvfB1Jt0NDVfvGjf/Fo=", "dev": true, "requires": { - "depd": "1.1.1", - "on-headers": "1.0.1" + "depd": "~1.1.0", + "on-headers": "~1.0.1" }, "dependencies": { "depd": { @@ -2043,6 +2927,21 @@ } } }, + "rewiremock": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.7.7.tgz", + "integrity": "sha512-84BnycBgnx32Ohn2VTcCJ2K3lMq0RoiLamRphARmrRz19lrOT1kuAtW6AJ3BR3UdbZVjwJ7nTol5YYI89vFc3Q==", + "dev": true, + "requires": { + "compare-module-exports": "^2.0.0", + "lodash.some": "^4.6.0", + "lodash.template": "^4.4.0", + "node-libs-browser": "^2.1.0", + "path-parse": "^1.0.5", + "wipe-node-cache": "^1.1.0", + "wipe-webpack-cache": "^1.0.3" + } + }, "right-align": { "version": "0.1.3", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/right-align/-/right-align-0.1.3.tgz", @@ -2050,7 +2949,17 @@ "dev": true, "optional": true, "requires": { - "align-text": "0.1.4" + "align-text": "^0.1.1" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" } }, "rndm": { @@ -2061,7 +2970,7 @@ }, "rsa-pem-from-mod-exp": { "version": "0.8.4", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.4.tgz", + "resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.4.tgz", "integrity": "sha1-NipCxtMEBW1JOz8SvOq7LGV2ptQ=" }, "safe-buffer": { @@ -2081,18 +2990,18 @@ "integrity": "sha1-UY+SGusFYK7H3KspkLFM9vPM5d4=", "dev": true, "requires": { - "debug": "2.2.0", - "depd": "1.0.1", + "debug": "~2.2.0", + "depd": "~1.0.1", "destroy": "1.0.3", "escape-html": "1.0.2", - "etag": "1.7.0", + "etag": "~1.7.0", "fresh": "0.3.0", - "http-errors": "1.3.1", + "http-errors": "~1.3.1", "mime": "1.3.4", "ms": "0.7.1", - "on-finished": "2.3.0", - "range-parser": "1.0.3", - "statuses": "1.2.1" + "on-finished": "~2.3.0", + "range-parser": "~1.0.2", + "statuses": "~1.2.1" }, "dependencies": { "destroy": { @@ -2136,10 +3045,10 @@ "integrity": "sha1-3UGeJo3gEqtysxnTN/IQUBP5OB8=", "dev": true, "requires": { - "etag": "1.7.0", + "etag": "~1.7.0", "fresh": "0.3.0", "ms": "0.7.2", - "parseurl": "1.3.2" + "parseurl": "~1.3.1" }, "dependencies": { "ms": { @@ -2156,13 +3065,13 @@ "integrity": "sha1-egV/xu4o3GP2RWbl+lexEahq7NI=", "dev": true, "requires": { - "accepts": "1.2.13", + "accepts": "~1.2.13", "batch": "0.5.3", - "debug": "2.2.0", - "escape-html": "1.0.3", - "http-errors": "1.3.1", - "mime-types": "2.1.17", - "parseurl": "1.3.2" + "debug": "~2.2.0", + "escape-html": "~1.0.3", + "http-errors": "~1.3.1", + "mime-types": "~2.1.9", + "parseurl": "~1.3.1" }, "dependencies": { "escape-html": { @@ -2179,8 +3088,8 @@ "integrity": "sha1-zlpuzTEB/tXsCYJ9rCKpwpv7BTU=", "dev": true, "requires": { - "escape-html": "1.0.3", - "parseurl": "1.3.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.1", "send": "0.13.2" }, "dependencies": { @@ -2223,18 +3132,18 @@ "integrity": "sha1-dl52B8gFVFK7pvCwUllTUJhgNt4=", "dev": true, "requires": { - "debug": "2.2.0", - "depd": "1.1.1", - "destroy": "1.0.4", - "escape-html": "1.0.3", - "etag": "1.7.0", + "debug": "~2.2.0", + "depd": "~1.1.0", + "destroy": "~1.0.4", + "escape-html": "~1.0.3", + "etag": "~1.7.0", "fresh": "0.3.0", - "http-errors": "1.3.1", + "http-errors": "~1.3.1", "mime": "1.3.4", "ms": "0.7.1", - "on-finished": "2.3.0", - "range-parser": "1.0.3", - "statuses": "1.2.1" + "on-finished": "~2.3.0", + "range-parser": "~1.0.3", + "statuses": "~1.2.1" } }, "statuses": { @@ -2245,12 +3154,28 @@ } } }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "sntp": { "version": "1.0.9", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/sntp/-/sntp-1.0.9.tgz", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", "requires": { - "hoek": "2.16.3" + "hoek": "2.x.x" } }, "source-map": { @@ -2260,12 +3185,12 @@ "dev": true, "optional": true, "requires": { - "amdefine": "1.0.1" + "amdefine": ">=0.0.4" } }, "sprintf-js": { "version": "1.1.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/sprintf-js/-/sprintf-js-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" }, "sshpk": { @@ -2273,14 +3198,14 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/sshpk/-/sshpk-1.13.1.tgz", "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" }, "dependencies": { "assert-plus": { @@ -2296,20 +3221,118 @@ "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", "dev": true }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "stream-counter": { "version": "0.2.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/stream-counter/-/stream-counter-0.2.0.tgz", "integrity": "sha1-3tJmVWMZyLDiIoErnPOyb6fZR94=", "dev": true, "requires": { - "readable-stream": "1.1.14" + "readable-stream": "~1.1.8" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "string_decoder": { "version": "0.10.31", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "stringstream": { "version": "0.0.5", @@ -2318,25 +3341,43 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "supports-color": { "version": "2.0.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", "dev": true }, "topo": { "version": "1.1.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/topo/-/topo-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", "requires": { - "hoek": "2.16.3" + "hoek": "2.x.x" } }, "tough-cookie": { @@ -2344,7 +3385,7 @@ "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/tough-cookie/-/tough-cookie-2.3.2.tgz", "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", "requires": { - "punycode": "1.4.1" + "punycode": "^1.4.1" } }, "tsscmp": { @@ -2353,17 +3394,28 @@ "integrity": "sha1-fcSjOvcVgatDN9qR2FylQn69mpc=", "dev": true }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz", + "integrity": "sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA==" + }, "tunnel-agent": { "version": "0.6.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { "version": "0.14.5", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/tweetnacl/-/tweetnacl-0.14.5.tgz", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, @@ -2373,7 +3425,7 @@ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "dev": true, "requires": { - "prelude-ls": "1.1.2" + "prelude-ls": "~1.1.2" } }, "type-detect": { @@ -2389,7 +3441,7 @@ "dev": true, "requires": { "media-typer": "0.3.0", - "mime-types": "2.0.14" + "mime-types": "~2.0.9" }, "dependencies": { "mime-db": { @@ -2404,7 +3456,7 @@ "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", "dev": true, "requires": { - "mime-db": "1.12.0" + "mime-db": "~1.12.0" } } } @@ -2416,9 +3468,9 @@ "dev": true, "optional": true, "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" }, "dependencies": { "source-map": { @@ -2443,7 +3495,7 @@ "integrity": "sha1-Otbzg2jG1MjHXsF2I/t5qh0HHYE=", "dev": true, "requires": { - "random-bytes": "1.0.0" + "random-bytes": "~1.0.0" } }, "unpipe": { @@ -2452,11 +3504,43 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, "url-join": { "version": "1.1.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/url-join/-/url-join-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=" }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "utils-merge": { "version": "1.0.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/utils-merge/-/utils-merge-1.0.0.tgz", @@ -2476,12 +3560,12 @@ }, "verror": { "version": "1.10.0", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/verror/-/verror-1.10.0.tgz", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" }, "dependencies": { "assert-plus": { @@ -2497,13 +3581,22 @@ "integrity": "sha1-L7HezUxGaqiLD5NBrzPcGv8keNU=", "dev": true }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, "which": { "version": "1.3.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/which/-/which-1.3.0.tgz", "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", "dev": true, "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, "window-size": { @@ -2513,6 +3606,18 @@ "dev": true, "optional": true }, + "wipe-node-cache": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wipe-node-cache/-/wipe-node-cache-1.1.0.tgz", + "integrity": "sha512-awSyDMC+XfWwO7LQPShGQGHQFSjaTlScpuvyqB8aS7IRnDlbd2DASIzTy0jKGmGBol567w/KDsVOHw/c/T9SIA==", + "dev": true + }, + "wipe-webpack-cache": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/wipe-webpack-cache/-/wipe-webpack-cache-1.0.3.tgz", + "integrity": "sha512-deVAI9EJZQZtqc19O5rSJH2cETv2HohetlE3MNAXiyHkgI/j2bYnJMLTHjnyDzAvWSwcNPIM6IsouqQ+zc02XA==", + "dev": true + }, "wordwrap": { "version": "1.0.0", "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/wordwrap/-/wordwrap-1.0.0.tgz", @@ -2527,7 +3632,7 @@ }, "xtend": { "version": "4.0.1", - "resolved": "https://fuselabs.pkgs.visualstudio.com/_packaging/FuseNPM/npm/registry/xtend/-/xtend-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "yargs": { @@ -2537,9 +3642,9 @@ "dev": true, "optional": true, "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", "window-size": "0.1.0" } } diff --git a/package.json b/package.json index 80b4548..0163ae8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubot-botframework", - "version": "0.10.1", + "version": "0.12.0-alpha1", "description": "Bot Framework adapter for Hubot", "main": "./src/adapter.coffee", "author": "Microsoft Corp.", @@ -18,6 +18,7 @@ }, "dependencies": { "botbuilder": ">=3.5.0", + "botbuilder-teams": ">=0.2.1", "parent-require": "^1.0.0" }, "peerDependencies": { @@ -31,6 +32,7 @@ "hubot": "^2.19.0", "istanbul": "^0.4.5", "mocha": "^3.5.0", - "mocha-lcov-reporter": "^1.3.0" + "mocha-lcov-reporter": "^1.3.0", + "rewiremock": "^3.7.7" } } diff --git a/src/adapter-middleware.coffee b/src/adapter-middleware.coffee index 69b6b12..4cfe67f 100644 --- a/src/adapter-middleware.coffee +++ b/src/adapter-middleware.coffee @@ -11,8 +11,10 @@ LogPrefix = "hubot-botframework-middleware:" class BaseMiddleware - constructor: (@robot) -> + constructor: (@robot, appId, appPassword) -> @robot.logger.info "#{LogPrefix} creating middleware..." + @appId = appId + @appPassword = appPassword toReceivable: (activity) -> throw new Error('toReceivable not implemented') @@ -21,6 +23,10 @@ class BaseMiddleware throw new Error('toSendable not implemented') class TextMiddleware extends BaseMiddleware + # TextMiddleware doesn't use invokes currently, so just return null + handleInvoke: (invokeEvent, connector) -> + return null + toReceivable: (activity) -> @robot.logger.info "#{LogPrefix} TextMiddleware toReceivable" address = activity.address @@ -42,6 +48,39 @@ class TextMiddleware extends BaseMiddleware } return message + + # Constructs a text message response to indicate an error to the user in the + # message channel they are using + constructErrorResponse: (activity, text) -> + payload = + type: 'message' + text: "#{text}" + address: activity?.address + return payload + + # Sends an error message back to the user if authorization isn't supported for the + # channel or prepares and sends the message to hubot for reception + maybeReceive: (activity, connector, authEnabled) -> + # Return an error to the user if the message channel doesn't support authorization + # and authorization is enabled + if authEnabled + @robot.logger.info "#{LogPrefix} Authorization isn\'t supported + for the channel error" + text = "Authorization isn't supported for this channel" + payload = @constructErrorResponse(activity, text) + @send(connector, payload) + else + event = @toReceivable activity + if event? + @robot.receive event + + # Sends the payload to the bot framework messaging channel + send: (connector, payload) -> + if !Array.isArray(payload) + payload = [payload] + connector.send payload, (err, _) -> + if err + throw err Middleware = { '*': TextMiddleware diff --git a/src/adapter.coffee b/src/adapter.coffee index 777dd7c..765f647 100644 --- a/src/adapter.coffee +++ b/src/adapter.coffee @@ -24,8 +24,26 @@ class BotFrameworkAdapter extends Adapter @appId = process.env.BOTBUILDER_APP_ID @appPassword = process.env.BOTBUILDER_APP_PASSWORD @endpoint = process.env.BOTBUILDER_ENDPOINT || "/api/messages" + @enableAuth = false + if process.env.HUBOT_TEAMS_ENABLE_AUTH? and process.env.HUBOT_TEAMS_ENABLE_AUTH == 'true' + @enableAuth = true + @initialAdmins = process.env.HUBOT_TEAMS_INITIAL_ADMINS robot.logger.info "#{LogPrefix} Adapter loaded. Using appId #{@appId}" + # Initial Admins should be required when auth is enabled + if @enableAuth + if @initialAdmins? + # If there isn't a list of authorized users in the brain, populate + # it with admins from the environment variable + if robot.brain.get("authorizedUsers") is null + robot.logger.info "#{LogPrefix} Restricting by name, setting admins" + authorizedUsers = {} + for admin in process.env.HUBOT_TEAMS_INITIAL_ADMINS.split(",") + authorizedUsers[admin.toLowerCase()] = true + robot.brain.set("authorizedUsers", authorizedUsers) + else + throw new Error("HUBOT_TEAMS_INITIAL_ADMINS is required for authorization") + @connector = new BotBuilder.ChatConnector { appId: @appId appPassword: @appPassword @@ -33,9 +51,19 @@ class BotFrameworkAdapter extends Adapter @connector.onEvent (events, cb) => @onBotEvents events, cb + @connector.onInvoke (events, cb) => @onInvoke events, cb + + + # Handles the invoke and passes an event to be handled, if needed + onInvoke: (invokeEvent, cb) -> + middleware = @using(invokeEvent.source) + event = middleware.handleInvoke(invokeEvent, @connector) + if event != null + @handleActivity(event) + using: (name) -> MiddlewareClass = Middleware.middlewareFor(name) - new MiddlewareClass(@robot) + new MiddlewareClass(@robot, @appId, @appPassword) onBotEvents: (activities, cb) -> @robot.logger.info "#{LogPrefix} onBotEvents" @@ -43,10 +71,12 @@ class BotFrameworkAdapter extends Adapter @handleActivity activity for activity in activities handleActivity: (activity) -> - @robot.logger.info "#{LogPrefix} Handling activity Channel: #{activity.source}; type: #{activity.type}" - event = @using(activity.source).toReceivable(activity) - if event? - @robot.receive event + @robot.logger.info "#{LogPrefix} Handling activity Channel: + #{activity.source}; type: #{activity.type}" + + # Construct the middleware + middleware = @using(activity.source) + middleware.maybeReceive(activity, @connector, @enableAuth) send: (context, messages...) -> @robot.logger.info "#{LogPrefix} send" @@ -54,15 +84,12 @@ class BotFrameworkAdapter extends Adapter reply: (context, messages...) -> @robot.logger.info "#{LogPrefix} reply" + for msg in messages activity = context.user.activity - payload = @using(activity.source).toSendable(context, msg) - if !Array.isArray(payload) - payload = [payload] - @connector.send payload, (err, _) -> - if err - throw err - + middleware = @using(activity.source) + payload = middleware.toSendable(context, msg) + middleware.send(@connector, payload) run: -> @robot.router.post @endpoint, @connector.listen() diff --git a/src/hubot-query-parts.coffee b/src/hubot-query-parts.coffee new file mode 100644 index 0000000..01271da --- /dev/null +++ b/src/hubot-query-parts.coffee @@ -0,0 +1,72 @@ +# A data structure used for constructing follow up commands in cards +# created for hubot repsonses sent to Microsoft Teams. Separates hubot +# queries into text parts and user provided input parts. The '/' +# character is used to indicate inputs with finite choices rather than +# accepting the input as a text field. +# Only queries used as a follow up query in the HubotResponseCards data +# structure are included, not all hubot commands. + +HubotQueryParts = { + "gho": + "textParts": [ + "hubot gho" + ] + "gho list (teams|repos|members)": + "textParts": [ + "hubot gho list " + ] + "inputParts": [ + "List what?/teams or repos or members" + ] + "gho list public repos": + "textParts": [ + "hubot gho list public repos" + ] + "gho create team ": + "textParts": [ + "hubot gho create team " + ] + "inputParts": [ + "What is the name of the team to create? (Max 1024 characters)" + ] + "gho create repo /": + "textParts": [ + "hubot gho create repo ", + "/" + ] + "inputParts": [ + "What is the name of the repo to create? (Max 1024 characters)", + "Public or private?/public or private" + ] + "gho add (members|repos) to team ": + "textParts": [ + "hubot gho add ", + " ", + " to team " + ] + "inputParts": [ + "Add members or repos?/members or repos", + "Input a comma separated list to add", + "What is the name of the team to add to?" + ] + "gho remove (repos|members) from team ": + "textParts": [ + "hubot gho remove ", + " ", + " from team " + ] + "inputParts": [ + "Remove members or repos?/members or repos", + "Input a comma separated list to remove", + "What is the name of the team to remove from?" + ] + "gho delete team ": + "textParts": [ + "hubot gho delete team " + ] + "inputParts": [ + "What is the name of the team to delete? (Max 1024 characters)" + ] +} + +module.exports = HubotQueryParts diff --git a/src/hubot-response-cards.coffee b/src/hubot-response-cards.coffee new file mode 100644 index 0000000..5116bf3 --- /dev/null +++ b/src/hubot-response-cards.coffee @@ -0,0 +1,298 @@ +# Contains helper methods and data structures for constructing and +# combining cards to return to Teams. + +HubotQueryParts = require './hubot-query-parts' + +maybeConstructResponseCard = (response, query) -> + # Check if response.text matches one of the reg exps in the LUT and + # construct a card if so. Otherwise, return null + for regex of HubotResponseCards + regexObject = new RegExp(regex) + if regexObject.test(query) + card = initializeAdaptiveCard(query) + card.content.body.push(addTextBlock(response.text)) + card.content.actions = getFollowUpButtons(query, regex) + return card + return null + +# Constructs an input card or returns null if the +# query doesn't need user input +maybeConstructMenuInputCard = (query) -> + queryParts = HubotQueryParts[query] + + # Check if the query needs a user input card + if queryParts.inputParts is undefined + return null + + shortQuery = constructShortQuery(query) + card = initializeAdaptiveCard(shortQuery) + + # Create the input fields of the sub card + for i in [0 ... queryParts.inputParts.length] + inputPart = queryParts.inputParts[i] + index = inputPart.search('/') + + # Create the prompt + promptEnd = inputPart.length + if index != -1 + promptEnd = index + card.content.body.push(addTextBlock("#{inputPart.substring(0, promptEnd)}")) + + # Create selector + if index != -1 + card.content.body.push(addSelector(query, inputPart.substring(index + 1), + query + " - input" + "#{i}")) + # Create text input + else + card.content.body.push(addTextInput(query + " - input" + "#{i}", inputPart)) + + # Create the submit button + data = { + 'queryPrefix': query + } + for i in [0 ... queryParts.textParts.length] + textPart = queryParts.textParts[i] + data[query + " - query" + "#{i}"] = textPart + + card.content.actions = [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': data + } + ] + return card + +# Initializes card structure +initializeAdaptiveCard = (query) -> + card = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "#{query}" + 'speak': "#{query}" + 'weight': 'bolder' + 'size': 'large' + } + ] + } + } + return card + +# Constructs an adaptive card text block to add to a card +addTextBlock = (text) -> + textBlock = { + 'type': 'TextBlock' + 'text': "#{text}" + 'speak': "#{text}" + } + return textBlock + +# Constructs an adaptive card input selector to add to a card +addSelector = (queryPrefix, choicesText, id) -> + selector = { + "type": "Input.ChoiceSet" + "id": id + "style": "compact" + } + choices = [] + for choice in choicesText.split(" or ") + choices.push({ + 'title': choice + 'value': choice + }) + selector.choices = choices + # Set the default value to the first choice + selector.value = choices[0].value + + return selector + +# Constructs an adaptive card text input to add to a card +addTextInput = (id, inputPart) -> + textInput = { + 'type': 'Input.Text' + 'id': id + 'speak': "#{inputPart}" + 'wrap': true + 'style': 'text' + 'maxLength': 1024 + } + return textInput + +# Creates an array of JSON adaptive card actions to use for +# a specific card +getFollowUpButtons = (query, regex) -> + actions = [] + for followUpQuery in HubotResponseCards[regex] + shortQuery = constructShortQuery(followUpQuery) + action = { + 'title': shortQuery + } + queryParts = HubotQueryParts[followUpQuery] + + # Doesn't need user input, just run the command when the + # follow up button is pressed + if queryParts.inputParts is undefined + action.type = 'Action.Submit' + action.data = { + 'queryPrefix': followUpQuery + } + + # Add the text parts to the data field of the action + for i in [0 ... queryParts.textParts.length] + textPart = queryParts.textParts[i] + action.data[followUpQuery + " - query" + "#{i}"] = textPart + + # Construct a card to show with input fields for each user input + # and a submit button containing the text parts + else + action.type = 'Action.ShowCard' + # Add the title for the sub card + action.card = { + 'type': 'AdaptiveCard' + 'body': [ + { + 'type': 'TextBlock' + 'text': "#{shortQuery}" + 'speak': "#{shortQuery}" + 'weight': 'bolder' + 'size': 'large' + } + ] + } + + # Create the input fields of the sub card + for i in [0 ... queryParts.inputParts.length] + inputPart = queryParts.inputParts[i] + index = inputPart.search('/') + + # Create the prompt + promptEnd = inputPart.length + if index != -1 + promptEnd = index + action.card.body.push(addTextBlock(inputPart.substring(0, promptEnd))) + + # Create selector + if index != -1 + action.card.body.push(addSelector(followUpQuery, \ + inputPart.substring(index + 1), \ + followUpQuery + " - input" + "#{i}")) + # Create text input + else + action.card.body.push(addTextInput(followUpQuery + " - input" + "#{i}", \ + inputPart)) + + # Create the submit button in the sub card + data = { + 'queryPrefix': followUpQuery + } + for i in [0 ... queryParts.textParts.length] + textPart = queryParts.textParts[i] + data[followUpQuery + " - query" + "#{i}"] = textPart + + action.card.actions = [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': data + } + ] + + # Add the action to actions + actions.push(action) + + return actions + +# Appends the card body of card2 to card1, skipping +# duplicate card body blocks, and returns card1. In the +# case that both card bodies are undefined +appendCardBody = (card1, card2) -> + if card2.content.body is undefined + return card1 + + if card1.content.body is undefined + card1.content.body = card2.content.body + return card1 + + for newBlock in card2.content.body + hasBlock = false + for storedBlock in card1.content.body + if JSON.stringify(storedBlock) == JSON.stringify(newBlock) + hasBlock = true + break + + if not hasBlock + card1.content.body.push(newBlock) + return card1 + +# Appends the card actions of card2 to those of card1, skipping +# actions which card1 already contains +appendCardActions = (card1, card2) -> + if card2.content.actions is undefined + return card1 + + if card1.content.actions is undefined + card1.content.actions = card2.content.actions + return card1 + + for newAction in card2.content.actions + hasAction = false + for storedAction in card1.content.actions + if JSON.stringify(storedAction) == JSON.stringify(newAction) + hasAction = true + break + + # if not in storedActions, add it + if not hasAction + card1.content.actions.push(newAction) + return card1 + +# Helper method to create a short version of the command by including only the +# start of the command to the first user input marked by ( or < +constructShortQuery = (query) -> + shortQueryEnd = query.search(new RegExp("[(<]")) + if shortQueryEnd == -1 + shortQueryEnd = query.length + shortQuery = query.substring(0, shortQueryEnd) + return shortQuery.trim() + +# HubotResponseCards maps from regex's of hubot queries to an array of follow up hubot +# queries stored as strings +HubotResponseCards = { + "(.+) gho list (teams|repos|members)": [ + "gho list (teams|repos|members)", + "gho list public repos" + ] + "(.+) gho create team (.+){1,1024}": [ + "gho add (members|repos) to team ", + "gho list (teams|repos|members)", + "gho delete team " + ] + "(.+) gho create repo [^/]{1,1024}(|/(|private|public))$": [ + "gho add (members|repos) to team ", + "gho list (teams|repos|members)" + ] + "(.+) gho add (repos|members) (.+)(,.)* to team (.+){1,1024}": [ + "gho remove (repos|members) from team " + ] + "(.+) gho remove (repos|members) (.+)(,.)* from team (.+){1,1024}": [ + "gho add (members|repos) to team " + ] + "(.+) gho delete team (.+){1,1024}": [ + "gho create team ", + "gho list (teams|repos|members)" + ] +} + +module.exports = { + maybeConstructResponseCard, + maybeConstructMenuInputCard, + appendCardBody, + appendCardActions +} diff --git a/src/msteams-middleware.coffee b/src/msteams-middleware.coffee index a0f8fc2..a5f52f5 100644 --- a/src/msteams-middleware.coffee +++ b/src/msteams-middleware.coffee @@ -17,25 +17,46 @@ # 3. Properly handles chat vs. channel messages # 4. Optionally filters out messages from outside the tenant # 5. Properly handles image responses. +# 6. Generates adaptive cards with follow up buttons for specific commands +# 7. Optionally restricts authorization to Hubot to a defined list of users # # Author: # billbliss # +BotBuilderTeams = require 'botbuilder-teams' +HubotResponseCards = require './hubot-response-cards' +HubotQueryParts = require './hubot-query-parts' { Robot, TextMessage, Message, User } = require 'hubot' { BaseMiddleware, registerMiddleware } = require './adapter-middleware' LogPrefix = "hubot-msteams:" + class MicrosoftTeamsMiddleware extends BaseMiddleware - constructor: (@robot) -> + constructor: (@robot, appId, appPassword) -> super(@robot) + @appId = appId + @appPassword = appPassword @allowedTenants = [] if process.env.HUBOT_OFFICE365_TENANT_FILTER? @allowedTenants = process.env.HUBOT_OFFICE365_TENANT_FILTER.split(",") - @robot.logger.info("#{LogPrefix} Restricting tenants to #{JSON.stringify(@allowedTenants)}") + @robot.logger.info("#{LogPrefix} Restricting tenants to \ + #{JSON.stringify(@allowedTenants)}") - toReceivable: (activity) -> + # If the invoke is due to a command that needs user input, sends a user input card + # otherwise, returns an event to handle, if needed, or null + handleInvoke: (invokeEvent, connector) -> + payload = @maybeConstructUserInputPrompt(invokeEvent) + if payload != null + @sendPayload(connector, payload) + return null + else + invokeEvent.text = invokeEvent.value.hubotMessage + delete invokeEvent.value + return invokeEvent + + toReceivable: (activity, chatMembers) -> @robot.logger.info "#{LogPrefix} toReceivable" # Drop the activity if it came from an unauthorized tenant @@ -47,16 +68,18 @@ class MicrosoftTeamsMiddleware extends BaseMiddleware user = getUser(activity) user = @robot.brain.userForId(user.id, user) - # We don't want to save the activity or room in the brain since its something that changes per chat. + # We don't want to save the activity or room in the brain since its + # something that changes per chat. user.activity = activity user.room = getRoomId(activity) - if activity.type == 'message' - activity = fixActivityForHubot(activity, @robot) - message = new TextMessage(user, activity.text, activity.address.id) - return message + # Return a generic message if the activity isn't a message or invoke + if activity.type != 'message' && activity.type != 'invoke' + return new Message(user) - return new Message(user) + activity = fixActivityForHubot(activity, @robot, chatMembers) + message = new TextMessage(user, activity.text, activity.address.id) + return message toSendable: (context, message) -> @robot.logger.info "#{LogPrefix} toSendable" @@ -64,15 +87,23 @@ class MicrosoftTeamsMiddleware extends BaseMiddleware response = message if typeof message is 'string' + # Trim leading or ending whitespace response = type: 'message' - text: message + text: message.trim() address: activity?.address - - imageAttachment = convertToImageAttachment(message) - if imageAttachment? + + # If the query sent by the user should trigger a card, + # construct the card to attach to the response + card = HubotResponseCards.maybeConstructResponseCard(response, activity.text) + if card != null delete response.text - response.attachments = [imageAttachment] + response.attachments = [card] + else + imageAttachment = convertToImageAttachment(message) + if imageAttachment? + delete response.text + response.attachments = [imageAttachment] response = fixMessageForTeams(response, @robot) @@ -82,6 +113,141 @@ class MicrosoftTeamsMiddleware extends BaseMiddleware return [typingMessage, response] + # Converts the activity to a hubot message and passes it to + # hubot for reception on success + maybeReceive: (activity, connector, authEnabled) -> + # Fetch the roster of members to do authorization, if enabled, based on UPN + teamsConnector = new BotBuilderTeams.TeamsChatConnector { + appId: @appId + appPassword: @appPassword + } + teamsConnector.fetchMembers activity?.address?.serviceUrl, \ + activity?.address?.conversation?.id, (err, chatMembers) => + if err + throw err + # Return with unauthorized error as true if auth is enabled and the user who sent + # the message is not authorized + if authEnabled + authorizedUsers = @robot.brain.get("authorizedUsers") + user = getUser(activity) + senderUPN = getSenderUPN(user, chatMembers).toLowerCase() + if senderUPN is undefined or authorizedUsers[senderUPN] is undefined + @robot.logger.info "#{LogPrefix} Unauthorized user; returning error" + text = "You are not authorized to send commands to hubot. + To gain access, talk to your admins:" + errorResponse = @constructErrorResponse(activity, text, true) + @send(connector, errorResponse) + return + + # Add the sender's UPN to the activity + activity.address.user.userPrincipalName = senderUPN + + # Convert the message to a hubot understandable form and + # send to the robot on success + event = @toReceivable activity, chatMembers + if event? + @robot.receive event + + # Combines payloads then sends the combined payload to MS Teams + send: (connector, payload) -> + # The message is from Teams, so combine hubot responses + # received within the next 100 ms then send the combined + # response + if @robot.brain.get("justReceivedResponse") is null + @robot.brain.set("teamsResponse", payload) + @robot.brain.set("justReceivedResponse", true) + setTimeout(@sendPayload.bind(this), 100, connector, @robot.brain.get("teamsResponse")) + else + @combineResponses(@robot.brain.get("teamsResponse"), payload) + + sendPayload: (connector, payload) -> + if !Array.isArray(payload) + payload = [payload] + connector.send payload, (err, _) => + if err + throw err + @robot.brain.remove("teamsResponse") + @robot.brain.remove("justReceivedResponse") + + # Combines the text and attachments of multiple hubot messages sent in succession. + # Most of the first received response is kept, and the text and attachments of + # subsequent responses received within 100ms of the first are combined into the + # first response. Assumes inputs follow the format of the payload returned by + # toSendable + combineResponses: (storedPayload, newPayload) -> + storedMessage = storedPayload[1] + newMessage = newPayload[1] + + # Combine the payload text, if needed, separated by a break + if newMessage.text != undefined + if storedMessage.text != undefined + storedMessage.text = "#{storedMessage.text}\r\n#{newMessage.text}" + else + storedMessage.text = newMessage.text + + # Combine attachments, if needed + if newMessage.attachments != undefined + # If the stored message doesn't have attachments and the new one does, + # just store the new attachments + if storedMessage.attachments == undefined + storedMessage.attachments = newMessage.attachments + + # Otherwise, combine them + else + storedCard = searchForAdaptiveCard(storedMessage.attachments) + # If the stored message doesn't have an adaptive card, just append the new + # attachments + if storedCard == null + for attachment in newMessage.attachments + storedMessage.attachments.push(attachment) + else + for attachment in newMessage.attachments + # If it's not an adaptive card, just append it, otherwise + # combine the cards + if attachment.contentType != "application/vnd.microsoft.card.adaptive" + storedMessage.attachments.push(attachment) + else + storedCard = HubotResponseCards.appendCardBody(storedCard, \ + attachment) + storedCard = HubotResponseCards.appendCardActions(storedCard, \ + attachment) + + # Constructs a text message response to indicate an error to the user in the + # message channel they are using + constructErrorResponse: (activity, text, appendAdmins) -> + if appendAdmins + authorizedUsers = @robot.brain.get("authorizedUsers") + for userKey, isAdmin of authorizedUsers + if isAdmin + text = "#{text}\r\n- #{userKey}" + + payload = + type: 'message' + text: "#{text}" + address: activity?.address + + return packagePayload(activity, payload) + + # Constructs a response containing a card for user input if needed or null + # if user input is not needed + maybeConstructUserInputPrompt: (event) -> + query = event.value.hubotMessage + # Remove hubot from the beginning of the command if it's there + query = query.replace("hubot ", "") + + card = HubotResponseCards.maybeConstructMenuInputCard(query) + if card is null + return null + + message = + type: 'message' + address: event?.address + attachments: [ + card + ] + + return packagePayload(event, message) + ############################################################################# # Helper methods for generating richer messages ############################################################################# @@ -109,12 +275,26 @@ class MicrosoftTeamsMiddleware extends BaseMiddleware id: activity?.address?.user?.id, name: activity?.address?.user?.name, tenant: getTenantId(activity) + aadObjectId: getUserAadObjectId(activity) + userPrincipalName: activity?.address?.user?.userPrincipalName return user + + # Fetches the user's name from the activity + getUserName = (activity) -> + return activity?.address?.user?.name + + # Fetches the user's AAD Object Id from the activity + getUserAadObjectId = (activity) -> + return activity?.address?.user?.aadObjectId # Fetches the room id from the activity getRoomId = (activity) -> return activity?.address?.conversation?.id + # Fetches the conversation type from the activity + getConversationType = (activity) -> + return activity?.address?.conversation?.conversationType + # Fetches the tenant id from the activity getTenantId = (activity) -> return activity?.sourceEvent?.tenant?.id @@ -124,35 +304,91 @@ class MicrosoftTeamsMiddleware extends BaseMiddleware entities = activity?.entities || [] if not Array.isArray(entities) entities = [entities] - return entities.filter((entity) -> entity.type == "mention" && (not userId? || userId == entity.mentioned?.id)) + return entities.filter((entity) -> entity.type == "mention" && \ + (not userId? || userId == entity.mentioned?.id)) + + # Returns the provided user's userPrincipalName (UPN) or null if one cannot be found + getSenderUPN = (user, chatMembers) -> + userAadObjectId = user.aadObjectId + for member in chatMembers + if userAadObjectId == member.objectId + return member.userPrincipalName + return null # Fixes the activity to have the proper information for Hubot - # 1. Replaces all occurances of the channel's bot at mention name with the configured name in hubot. + # 1. Constructs the text command to send to hubot if the event is from a + # submit on an adaptive card (containing the value property). + # 2. Replaces all occurrences of the channel's bot at mention name with the configured + # name in hubot. # The hubot's configured name might not be the same name that is sent from the chat service in # the activity's text. - # 2. Prepends hubot's name to the message if this is a direct message. - fixActivityForHubot = (activity, robot) -> + # 3. Replaces all occurrences of @ mentions to users with their aad object id if the user is + # on the roster of chanenl members from Teams. If a mentioned user is not in the chat roster, + # the mention is replaced with their name. + # 4. Trims all whitespace and newlines from the beginning and end of the text. + # 5. Prepends hubot's name to the message for personal messages if it's not already + # there + fixActivityForHubot = (activity, robot, chatMembers) -> + # If activity.value exists, the command is from a follow up button press on + # a card and the correct query to send to hubot should be constructed + if activity?.value != undefined + data = activity.value + + # Used to uniquely identify command parts since adaptive cards + # don't differentiate between different sub-cards' data fields + queryPrefix = data.queryPrefix + + # Get the first command part. A command always begins with a text part + # since if activity.value is populated, the command is from a card we + # created, and we always include at least hubot at the beginning of + # these commands + text = data[queryPrefix + " - query0"] + text = text.replace("hubot", robot.name) + + # If there are inputs, add those and the next query part + # if there is another query part + i = 0 + input = data[queryPrefix + " - input#{i}"] + while input != undefined + text = text + input + nextTextPart = data[queryPrefix + " - query" + (i + 1)] + if nextTextPart != undefined + text = text + nextTextPart + i++ + input = data[queryPrefix + " - input#{i}"] + + # Set the constructed query as the text of the activity + activity.text = text + if not activity?.text? || typeof activity.text isnt 'string' return activity + myChatId = activity?.address?.bot?.id if not myChatId? return activity - # replace all @ mentions with the robot's name + # Replace all @ mentions to the bot with the bot's name, and replace + # all @ mentions of users with a known aad object id with their aad + # object id. mentions = getMentions(activity) for mention in mentions mentionTextRegExp = new RegExp(escapeRegExp(mention.text), "gi") replacement = mention.mentioned.name if mention.mentioned.id == myChatId replacement = robot.name - + if chatMembers != undefined + for member in chatMembers + if mention.mentioned.id == member.id + replacement = member.userPrincipalName activity.text = activity.text.replace(mentionTextRegExp, replacement) + + # Remove leading/trailing whitespace and newlines + activity.text = activity.text.trim() - # prepends the robot's name for direct messages - roomId = getRoomId(activity) - if roomId? and not roomId.startsWith("19:") and not activity.text.startsWith(robot.name) + # prepends the robot's name for direct messages if it's not already there + if getConversationType(activity) == 'personal' && activity.text.search(robot.name) != 0 activity.text = "#{robot.name} #{activity.text}" - + return activity slackMentionRegExp = /<@([^\|>]*)\|?([^>]*)>/g @@ -161,10 +397,11 @@ class MicrosoftTeamsMiddleware extends BaseMiddleware # 1. Replaces all slack @ mentions with Teams @ mentions # Slack mentions take the form of <@[username or id]|[mention text]> # We have to convert this into a mention object which needs the id. + # 2. Escapes all < to render 'hubot help' properly + # 3. Replaces all newlines with break tags to render line breaks properly fixMessageForTeams = (response, robot) -> if not response?.text? return response - mentions = [] while match = slackMentionRegExp.exec(response.text) foundUser = null @@ -189,11 +426,42 @@ class MicrosoftTeamsMiddleware extends BaseMiddleware response.text = response.text.replace(mentionTextRegExp, mention.text) delete mention.full response.entities = mentions + + # Escape < in hubot help commands, determined by the response text + # starting with 'hubot' + if response.text.search("hubot") == 0 + response.text = escapeLessThan(response.text) + + # Replace \n with html
for rendering breaks in Teams + response.text = escapeNewLines(response.text) + return response escapeRegExp = (str) -> return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") + escapeLessThan = (str) -> + str = str.replace(/ + return str.replace(/\n/g, "
") + + # Helper method for retrieving the first adaptive card from a list of + # attachments or null if there are none + searchForAdaptiveCard = (attachments) -> + card = null + for attachment in attachments + if attachment.contentType == "application/vnd.microsoft.card.adaptive" + card = attachment + return card + + packagePayload = (activity, message) -> + typing = + type: 'typing' + address: activity?.address + return [typing, message] + registerMiddleware 'msteams', MicrosoftTeamsMiddleware diff --git a/test/adapter-middleware.test.coffee b/test/adapter-middleware.test.coffee index d3368fa..9cc8454 100644 --- a/test/adapter-middleware.test.coffee +++ b/test/adapter-middleware.test.coffee @@ -3,6 +3,7 @@ expect = chai.expect { TextMessage, Message, User } = require 'hubot' MockRobot = require './mock-robot' { BaseMiddleware, TextMiddleware, middlewareFor } = require '../src/adapter-middleware' +BotBuilderTeams = require('./mock-botbuilder-teams') describe 'middlewareFor', -> it 'should return Middleware for null', -> @@ -76,9 +77,48 @@ describe 'BaseMiddleware', -> ).to.throw() describe 'TextMiddleware', -> + describe 'handleInvoke', -> + robot = null + event = null + connector = null + appId = 'a-app-id' + appPassword = 'a-app-password' + beforeEach -> + robot = new MockRobot + event = + type: 'invoke' + text: 'Bot do something and tell User about it' + agent: 'tests' + source: '*' + address: + conversation: + id: "conversation-id" + bot: + id: "bot-id" + user: + id: "user-id" + name: "user-name" + connector = + send: () -> {} + + it 'should return null', -> + # Setup + middleware = new TextMiddleware(robot, appId, appPassword) + + # Action + result = null + expect(() -> + result = middleware.handleInvoke(event, connector) + ).to.not.throw() + + # Assert + expect(result).to.be.null + describe 'toReceivable', -> robot = null event = null + appId = 'a-app-id' + appPassword = 'a-app-password' beforeEach -> robot = new MockRobot event = @@ -98,7 +138,7 @@ describe 'TextMiddleware', -> it 'return generic message when appropriate type is not found', -> # Setup event.type = 'typing' - middleware = new TextMiddleware(robot) + middleware = new TextMiddleware(robot, appId, appPassword) # Action receivable = null @@ -111,7 +151,7 @@ describe 'TextMiddleware', -> it 'return message when type is message', -> # Setup - middleware = new TextMiddleware(robot) + middleware = new TextMiddleware(robot, appId, appPassword) # Action receivable = null @@ -126,6 +166,8 @@ describe 'TextMiddleware', -> robot = null message = null context = null + appId = 'a-app-id' + appPassword = 'a-app-password' beforeEach -> robot = new MockRobot context = @@ -149,7 +191,7 @@ describe 'TextMiddleware', -> it 'should create message object for string messages', -> # Setup - middleware = new TextMiddleware(robot) + middleware = new TextMiddleware(robot, appId, appPassword) # Action sendable = null @@ -169,7 +211,7 @@ describe 'TextMiddleware', -> # Setup message = type: "some message type" - middleware = new TextMiddleware(robot) + middleware = new TextMiddleware(robot, appId, appPassword) # Action sendable = null @@ -180,3 +222,207 @@ describe 'TextMiddleware', -> # Verify expected = message expect(sendable).to.deep.equal(expected) + + describe 'maybeReceive', -> + robot = null + middleware = null + authEnabled = true + connector = null + event = null + + beforeEach -> + robot = new MockRobot + middleware = new TextMiddleware(robot, 'a-app-id', 'a-app-password') + connector = + send: (payload, cb) -> + robot.brain.set("payload", payload) + authEnabled = true + event = + type: 'message' + text: 'Bot do something Bot and tell User about it' + agent: 'tests' + source: 'msteams' + entities: [ + type: "mention" + text: "Bot" + mentioned: + id: "bot-id" + name: "bot-name" + , + type: "mention" + text: "User" + mentioned: + id: "user-id" + name: "user-name" + ] + sourceEvent: + tenant: + id: "tenant-id" + address: + conversation: + id: "19:conversation-id" + bot: + id: "bot-id" + user: + id: "user-id" + name: "user-name" + aadObjectId: "eight888-four-4444-fore-twelve121212" + userPrincipalName: "user-UPN" + serviceUrl: 'url-serviceUrl/a-url' + + it 'should return authorization not supported error when auth is enabled', -> + # Setup + + # Action + expect(() -> + middleware.maybeReceive(event, connector, authEnabled) + ).to.not.throw() + + # Assert + resultEvent = robot.brain.get("event") + expect(resultEvent).to.be.null + resultPayload = robot.brain.get("payload") + expect(resultPayload).to.be.a('Array') + expect(resultPayload.length).to.eql 1 + expect(resultPayload[0].text).to.eql "Authorization isn't supported for this channel" + + it 'should work when auth is not enabled', -> + # Setup + authEnabled = false + + # Action + expect(() -> + middleware.maybeReceive(event, connector, authEnabled) + ).to.not.throw() + + # Assert + resultEvent = robot.brain.get("event") + expect(resultEvent).to.not.be.null + expect(resultEvent).to.be.a('Object') + resultPayload = robot.brain.get("payload") + expect(resultPayload).to.be.null + + describe 'constructErrorResponse', -> + it 'return a proper payload with the text of the error', -> + # Setup + robot = new MockRobot + middleware = new TextMiddleware(robot, 'a-app-id', 'a-app-password') + event = + type: 'message' + text: 'Bot do something and tell User about it' + agent: 'tests' + source: '*' + address: + conversation: + id: "conversation-id" + bot: + id: "bot-id" + user: + id: "user-id" + name: "user-name" + + # Action + result = null + expect(() -> + result = middleware.constructErrorResponse(event, "an error message") + ).to.not.throw() + + # Assert + expect(result).to.eql { + type: 'message' + text: 'an error message' + address: + conversation: + id: "conversation-id" + bot: + id: "bot-id" + user: + id: "user-id" + name: "user-name" + } + + describe 'send', -> + robot = null + middleware = null + connector = null + payload = null + cb = () -> {} + + beforeEach -> + robot = new MockRobot + middleware = new TextMiddleware(robot, 'a-app-id', 'a-app-password') + connector = new BotBuilderTeams.TeamsChatConnector({ + appId: 'a-app-id' + appPassword: 'a-app-password' + }) + connector.send = (payload, cb) -> + robot.brain.set("payload", payload) + + payload = { + type: 'message' + text: "" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + } + + it 'should package non-array payload in array before sending', -> + # Setup + expected = [{ + type: 'message' + text: "" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + }] + + # Action + expect(() -> + middleware.send(connector, payload) + ).to.not.throw() + + # Assert + result = robot.brain.get("payload") + expect(result).to.deep.eql(expected) + + + it 'should pass payload array through unchanged', -> + # Setup + payload = [payload] + expected = [{ + type: 'message' + text: "" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + }] + + # Action + expect(() -> + middleware.send(connector, payload) + ).to.not.throw() + + # Assert + result = robot.brain.get("payload") + expect(result).to.deep.eql(expected) diff --git a/test/adapter.test.coffee b/test/adapter.test.coffee new file mode 100644 index 0000000..740eea1 --- /dev/null +++ b/test/adapter.test.coffee @@ -0,0 +1,63 @@ +chai = require 'chai' +expect = chai.expect +{ TextMessage, Message, Robot, User } = require 'hubot' +BotFrameworkAdapter = require '../src/adapter' +MockRobot = require './mock-robot' + + +describe 'Main Adapter', -> + describe 'Test Authorization Setup', -> + beforeEach -> + process.env.HUBOT_TEAMS_INITIAL_ADMINS = 'an-1_20@em.ail,authorized_user@email.la' + process.env.HUBOT_TEAMS_ENABLE_AUTH = 'true' + + it 'should not set initial admins when auth enable is not set', -> + # Setup + delete process.env.HUBOT_TEAMS_ENABLE_AUTH + robot = new MockRobot + + # Action + expect(() -> + adapter = BotFrameworkAdapter.use(robot) + ).to.not.throw() + + # Assert + expect(robot.brain.get("authorizedUsers")).to.be.null + + it 'should not set initial admins when auth is not enabled', -> + # Setup + process.env.HUBOT_TEAMS_ENABLE_AUTH = 'false' + robot = new MockRobot + + # Action + expect(() -> + adapter = BotFrameworkAdapter.use(robot) + ).to.not.throw() + + # Assert + expect(robot.brain.get("authorizedUsers")).to.be.null + + it 'should throw error when auth is enabled and initial admins', -> + # Setup + delete process.env.HUBOT_TEAMS_INITIAL_ADMINS + robot = new MockRobot + + # Action and Assert + expect(() -> + adapter = BotFrameworkAdapter.use(robot) + ).to.throw() + + it 'should set initial admins when auth is enabled', -> + # Setup + robot = new MockRobot + + # Action + expect(() -> + adapter = BotFrameworkAdapter.use(robot) + ).to.not.throw() + + # Assert + expect(robot.brain.get("authorizedUsers")).to.eql { + 'an-1_20@em.ail': true + 'authorized_user@email.la': true + } diff --git a/test/hubot-response-cards.test.coffee b/test/hubot-response-cards.test.coffee new file mode 100644 index 0000000..094bbb3 --- /dev/null +++ b/test/hubot-response-cards.test.coffee @@ -0,0 +1,708 @@ +# Description: +# Tests for helper methods used to construct Adaptive Cards for specific hubot +# commands when used with the Botframework adapter + +chai = require 'chai' +expect = chai.expect + +HubotResponseCards = require '../src/hubot-response-cards' + +describe 'HubotResponseCards', -> + describe 'maybeConstructResponseCard', -> + query = null + response = null + beforeEach -> + query = 'hubot gho create team team-name' + response = { + type: 'message', + text: 'The team: `team-name` was successfully created', + address: { + id: 'id', + channelId: 'msteams', + user: { + id: 'user-id', + name: 'user-name', + aadObjectId: 'user-aad-id' + userPrincipalName: 'user-UPN' + }, + conversation: { + conversationType: 'conversation-type', + id: 'conversation-id' + }, + bot: { + id: 'botframework-bot-id', + name: 'botframework-bot-name' + }, + serviceUrl: 'a-service-url' + } + } + + it 'should not construct response card for the query', -> + # Setup + query = 'hubot ping' + + # Action + card = null + expect(() -> + card = HubotResponseCards.maybeConstructResponseCard(response, query) + ).to.not.throw() + + # Assert + expect(card).to.be.null + + it 'should construct response card for the query', -> + # Setup + query = 'hubot gho create team team-name' + followUp1 = 'gho add (members|repos) to team ' + followUp2 = 'gho list (teams|repos|members)' + followUp3 = 'gho delete team ' + expected = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "#{query}" + 'speak': "#{query}" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': "#{response.text}" + 'speak': "#{response.text}" + } + ], + 'actions': [ + { + "title": "gho add" + "type": "Action.ShowCard" + "card": { + "type": "AdaptiveCard" + "body": [ + { + 'type': 'TextBlock' + 'text': "gho add" + 'speak': "gho add" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': 'Add members or repos?' + 'speak': "Add members or repos?" + }, + { + "type": "Input.ChoiceSet" + "id": "#{followUp1} - input0" + "style": "compact" + "value": "members" + "choices": [ + { + "title": "members" + "value": "members" + }, + { + "title": "repos" + "value": "repos" + } + ] + }, + { + 'type': 'TextBlock' + 'text': 'Input a comma separated list to add' + 'speak': "Input a comma separated list to add" + }, + { + 'type': 'Input.Text' + 'id': "#{followUp1} - input1" + 'speak': 'Input a comma separated list to add' + 'wrap': true + 'style': 'text' + 'maxLength': 1024 + }, + { + 'type': 'TextBlock' + 'text': 'What is the name of the team to add to?' + 'speak': "What is the name of the team to add to?" + }, + { + 'type': 'Input.Text' + 'id': "#{followUp1} - input2" + 'speak': 'What is the name of the team to add to?' + 'wrap': true + 'style': 'text' + 'maxLength': 1024 + } + ], + 'actions': [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + 'queryPrefix': "#{followUp1}" + "#{followUp1} - query0": 'hubot gho add ' + "#{followUp1} - query1": ' ' + "#{followUp1} - query2": ' to team ' + } + } + ] + } + }, + { + "title": "gho list" + "type": "Action.ShowCard" + "card": { + "type": "AdaptiveCard" + "body": [ + { + 'type': 'TextBlock' + 'text': "gho list" + 'speak': "gho list" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': 'List what?' + 'speak': "List what?" + }, + { + "type": "Input.ChoiceSet" + "id": "#{followUp2} - input0" + "style": "compact" + "value": "teams" + "choices": [ + { + "title": "teams" + "value": "teams" + }, + { + "title": "repos" + "value": "repos" + }, + { + "title": "members" + "value": "members" + } + ] + } + ], + 'actions': [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + 'queryPrefix': "#{followUp2}" + "#{followUp2} - query0": 'hubot gho list ' + } + } + ] + } + }, + { + "title": "gho delete team" + "type": "Action.ShowCard" + "card": { + "type": "AdaptiveCard" + "body": [ + { + 'type': 'TextBlock' + 'text': "gho delete team" + 'speak': "gho delete team" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': 'What is the name of the team to delete? (Max 1024 characters)' + 'speak': "What is the name of the team to delete? (Max 1024 characters)" + }, + { + 'type': 'Input.Text' + 'id': "#{followUp3} - input0" + 'speak': "What is the name of the team to delete? (Max 1024 characters)" + 'wrap': true + 'style': 'text' + 'maxLength': 1024 + } + ], + 'actions': [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + 'queryPrefix': "#{followUp3}" + "#{followUp3} - query0": 'hubot gho delete team ' + } + } + ] + } + } + ] + } + } + + # Action + card = null + expect(() -> + card = HubotResponseCards.maybeConstructResponseCard(response, query) + ).to.not.throw() + + # Assert + expect(card).to.eql(expected) + + describe 'maybeConstructMenuInputCard', -> + it 'should not construct menu input card for the query', -> + # Setup + query = 'ping' + + # Action + result = null + expect(() -> + result = HubotResponseCards.maybeConstructMenuInputCard(query) + ) + + # Assert + expect(result).to.be.null + + it 'should construct menu input card for the query', -> + # Setup + query = 'gho list (teams|repos|members)' + expected = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "gho list" + 'speak': "gho list" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': 'List what?' + 'speak': "List what?" + }, + { + "type": "Input.ChoiceSet" + "id": "gho list (teams|repos|members) - input0" + "style": "compact" + "value": "teams" + "choices": [ + { + "title": "teams" + "value": "teams" + }, + { + "title": "repos" + "value": "repos" + }, + { + "title": "members" + "value": "members" + } + ] + } + ], + 'actions': [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + 'queryPrefix': "gho list (teams|repos|members)" + "gho list (teams|repos|members) - query0": 'hubot gho list ' + } + } + ] + } + } + + # Action + result = null + expect(() -> + result = HubotResponseCards.maybeConstructMenuInputCard(query) + ).to.not.throw() + + # Assert + expect(result).to.eql(expected) + + describe 'appendCardBody', -> + card1 = null + card2 = null + expected = null + beforeEach -> + card1 = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "Card1" + 'speak': "Card1" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'Input.Text' + 'id': "the-same-id" + 'speak': "the same text" + 'wrap': true + 'style': 'text' + }, + { + 'type': 'TextBlock' + 'text': "This is unique to 1" + 'speak': "This is unique to 1" + }, + { + "type": "Input.ChoiceSet" + "id": "a-selector-unique-to-card1-id" + "style": "compact" + "choices": [ + { + "title": "Card 1 choice" + "value": "Card 1 choice" + }, + { + "title": "Another card 1 choice" + "value": "Another card 1 choice" + } + ] + "value": "Another card 1 choice" + } + ] + } + } + card2 = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "Card2" + 'speak': "Card2" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': "This is unique to 2" + 'speak': "This is unique to 2" + }, + { + 'type': 'Input.Text' + 'id': "the-same-id" + 'speak': "the same text" + 'wrap': true + 'style': 'text' + }, + { + "type": "Input.ChoiceSet" + "id": "a-selector-unique-to-card2-id" + "style": "compact" + "choices": [ + { + "title": "Card 2 choice" + "value": "Card 2 choice" + }, + { + "title": "Another card 2 choice" + "value": "Another card 2 choice" + } + ] + "value": "Another card 2 choice" + } + ] + } + } + expected = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "Card1" + 'speak': "Card1" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'Input.Text' + 'id': "the-same-id" + 'speak': "the same text" + 'wrap': true + 'style': 'text' + }, + { + 'type': 'TextBlock' + 'text': "This is unique to 1" + 'speak': "This is unique to 1" + }, + { + "type": "Input.ChoiceSet" + "id": "a-selector-unique-to-card1-id" + "style": "compact" + "choices": [ + { + "title": "Card 1 choice" + "value": "Card 1 choice" + }, + { + "title": "Another card 1 choice" + "value": "Another card 1 choice" + } + ] + "value": "Another card 1 choice" + } + ] + } + } + + it 'both cards don\'t have bodies, should return card1 unchanged', -> + # Setup + delete card1.content.body + delete card2.content.body + delete expected.content.body + + # Action + result = null + expect(() -> + result = HubotResponseCards.appendCardBody(card1, card2) + ).to.not.throw() + + # Assert + expect(result).to.deep.equal(expected) + + it 'card2 doesn\'t have a body, should return card1 unchanged', -> + # Setup + delete card2.content.body + + # Action + result = null + expect(() -> + result = HubotResponseCards.appendCardBody(card1, card2) + ).to.not.throw() + + # Assert + expect(result).to.deep.equal(expected) + + it 'card1 doesn\'t have a body, result body should equal card2\'s body', -> + # Setup + delete card1.content.body + expected.content.body = card2.content.body + + # Action + result = null + expect(() -> + result = HubotResponseCards.appendCardBody(card1, card2) + ).to.not.throw() + + # Assert + expect(result).to.deep.equal(expected) + + it 'both cards have bodies, should combine both bodies into card1 and remove duplicates', -> + # Setup + expected.content.body.push({ + 'type': 'TextBlock' + 'text': "Card2" + 'speak': "Card2" + 'weight': 'bolder' + 'size': 'large' + }) + expected.content.body.push({ + 'type': 'TextBlock' + 'text': "This is unique to 2" + 'speak': "This is unique to 2" + }) + expected.content.body.push({ + "type": "Input.ChoiceSet" + "id": "a-selector-unique-to-card2-id" + "style": "compact" + "choices": [ + { + "title": "Card 2 choice" + "value": "Card 2 choice" + }, + { + "title": "Another card 2 choice" + "value": "Another card 2 choice" + } + ] + "value": "Another card 2 choice" + }) + + # Action + result = null + expect(() -> + result = HubotResponseCards.appendCardBody(card1, card2) + ).to.not.throw() + + # Assert + expect(result).to.deep.equal(expected) + + describe 'appendCardActions', -> + card1 = null + card2 = null + expected = null + beforeEach -> + card1 = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "actions": [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + "a-shared-field": "shared" + } + }, + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + "a-field-card1": "a-value-card1" + } + } + ] + } + } + card2 = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "actions": [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + "a-shared-field": "shared" + } + }, + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + "a-field-card2": "a-value-card2" + } + }, + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + "a-shared-field": "shared" + } + } + ] + } + } + expected = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "actions": [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + "a-shared-field": "shared" + } + }, + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + "a-field-card1": "a-value-card1" + } + } + ] + } + } + + it 'both cards don\'t have actions, should return card1 unchanged', -> + # Setup + delete card1.content.actions + delete card2.content.actions + delete expected.content.actions + + # Action + result = null + expect(() -> + result = HubotResponseCards.appendCardActions(card1, card2) + ).to.not.throw() + + # Assert + expect(result).to.deep.equal(expected) + + it 'card2 doesn\'t have actions, should return card1 unchanged', -> + # Setup + delete card2.content.actions + + # Action + result = null + expect(() -> + result = HubotResponseCards.appendCardActions(card1, card2) + ).to.not.throw() + + # Assert + expect(result).to.deep.equal(expected) + + it 'card1 doesn\'t have actions, result actions should equal card2\'s actions', -> + # Setup + delete card1.content.actions + expected.content.actions = card2.content.actions + + # Action + result = null + expect(() -> + result = HubotResponseCards.appendCardActions(card1, card2) + ).to.not.throw() + + # Assert + expect(result).to.deep.equal(expected) + + it 'both cards have actions, should combine both actions into card1 and remove duplicates', -> + # Setup + expected.content.actions.push({ + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + "a-field-card2": "a-value-card2" + } + }) + + # Action + result = null + expect(() -> + result = HubotResponseCards.appendCardActions(card1, card2) + ).to.not.throw() + + # Assert + expect(result).to.deep.equal(expected) diff --git a/test/mocha.opts b/test/mocha.opts index 795baec..04973f1 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,3 @@ --compilers coffee:coffee-script/register --require coffee-coverage/register-istanbul ---recursive \ No newline at end of file +--recursive diff --git a/test/mock-botbuilder-teams.coffee b/test/mock-botbuilder-teams.coffee new file mode 100644 index 0000000..82a88d7 --- /dev/null +++ b/test/mock-botbuilder-teams.coffee @@ -0,0 +1,35 @@ +class TeamsChatConnector + constructor: (options) -> + @appId = options.appId + @appPassword = options.appPassword + + fetchMembers: (serviceUrl, conversationId, callback) -> + members = [ + { + id: 'user-id', + objectId: 'eight888-four-4444-fore-twelve121212', + name: 'user-name', + givenName: 'user-', + surname: 'name', + email: 'em@ai.l', + userPrincipalName: 'em@ai.l' + }, + { + id: 'user2-id', + objectId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + name: 'user2 two', + givenName: 'user2', + surname: 'two', + email: 'em@ai.l2', + userPrincipalName: 'em@ai.l2' + } + ] + + callback false, members + +BotBuilderTeams = { + TeamsChatConnector: TeamsChatConnector +} + +# module.exports = TeamsChatConnector +module.exports = BotBuilderTeams diff --git a/test/mock-robot.coffee b/test/mock-robot.coffee index e7c4e69..702598f 100644 --- a/test/mock-robot.coffee +++ b/test/mock-robot.coffee @@ -4,7 +4,35 @@ class MockRobot @logger = info: -> warn: -> + @commands = [ + "hubot a - does something a" + "hubot b - does something b" + ] @brain = - userForId: -> {} + data: {} + userForId: (id, options) -> + user = { + id: "#{id}" + name: "a-hubot-user-name" + } + if options is null + return user + else + for key of options + user[key] = options[key] + return user + users: -> [] + get: (key) -> + if @data is undefined + return null + for storedKey of @data + if key == storedKey + return @data[storedKey] + return null + set: (key, value) -> + @data[key] = value + + receive: (event) -> + @brain.data["event"] = event module.exports = MockRobot diff --git a/test/msteams-middleware.test.coffee b/test/msteams-middleware.test.coffee index 608c9e9..2d7ace4 100644 --- a/test/msteams-middleware.test.coffee +++ b/test/msteams-middleware.test.coffee @@ -1,20 +1,42 @@ chai = require 'chai' expect = chai.expect { TextMessage, Message, User } = require 'hubot' +rewiremock = require('rewiremock/node').default MockRobot = require './mock-robot' MicrosoftTeamsMiddleware = require '../src/msteams-middleware' describe 'MicrosoftTeamsMiddleware', -> - describe 'toReceivable', -> + describe 'handleInvoke', -> + rewiremock('botbuilder-teams').with(require('./mock-botbuilder-teams')) + BotBuilderTeams = null robot = null event = null + options = null + teamsChatConnector = null + authEnabled = false + appId = 'a-app-id' + appPassword = 'a-app-password' + cb = () -> {} + beforeEach -> - delete process.env.HUBOT_OFFICE365_TENANT_FILTER + rewiremock.enable() + BotBuilderTeams = require 'botbuilder-teams' + MicrosoftTeamsMiddleware = require '../src/msteams-middleware' + robot = new MockRobot + options = { + appId: 'botframework-app-id' + appPassword: 'botframework-app-password' + } + teamsChatConnector = new BotBuilderTeams.TeamsChatConnector(options) + teamsChatConnector.send = (payload) -> + robot.brain.set("payload", payload) + event = - type: 'message' + type: 'invoke' text: 'Bot do something Bot and tell User about it' - agent: 'tests' + value: + hubotMessage: 'gho list (teams|repos|members)' source: 'msteams' entities: [ type: "mention" @@ -34,161 +56,375 @@ describe 'MicrosoftTeamsMiddleware', -> id: "tenant-id" address: conversation: + isGroup: 'true' + conversationType: 'channel' id: "19:conversation-id" bot: id: "bot-id" user: id: "user-id" name: "user-name" + aadObjectId: 'eight888-four-4444-fore-twelve121212' + userPrincipalName: 'em@ai.l' + + afterEach -> + rewiremock.disable() + + it 'should send user input card for specific queries', -> + # Setup + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + + # Action + result = null + expect(() -> + result = teamsMiddleware.handleInvoke(event, teamsChatConnector) + ).to.not.throw() + + # Assert + expect(result).to.be.null + response = robot.brain.get("payload") + expect(response).to.be.a('Array') + expect(response.length).to.eql(2) + + it 'should return event to handle', -> + # Setup + event.value.hubotMessage = "gho list public repos" + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + + # Action + result = null + expect(() -> + result = teamsMiddleware.handleInvoke(event, teamsChatConnector) + ).to.not.throw() + + # Assert + expect(result).to.not.be.null + expect(result).to.be.a('Object') + response = robot.brain.get("payload") + expect(response).to.be.null + + describe 'toReceivable', -> + rewiremock('botbuilder-teams').with(require('./mock-botbuilder-teams')) + BotBuilderTeams = null + robot = null + event = null + options = null + teamsChatConnector = null + appId = 'a-app-id' + appPassword = 'a-app-password' + chatMembers = [ + { + id: 'user-id', + objectId: 'eight888-four-4444-fore-twelve121212', + name: 'user-name', + givenName: 'user-', + surname: 'name', + email: 'em@ai.l', + userPrincipalName: 'em@ai.l' + }, + { + id: 'user2-id', + objectId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + name: 'user2 two', + givenName: 'user2', + surname: 'two', + email: 'em@ai.l2', + userPrincipalName: 'em@ai.l2' + } + ] + + beforeEach -> + rewiremock.enable() + MicrosoftTeamsMiddleware = require '../src/msteams-middleware' + BotBuilderTeams = require 'botbuilder-teams' + + delete process.env.HUBOT_OFFICE365_TENANT_FILTER + robot = new MockRobot + options = { + appId: 'botframework-app-id' + appPassword: 'botframework-app-password' + } + teamsChatConnector = new BotBuilderTeams.TeamsChatConnector(options) + authEnabled = false + event = + type: 'message' + text: 'Bot do something Bot and tell User about it' + agent: 'tests' + source: 'msteams' + entities: [ + type: "mention" + text: "Bot" + mentioned: + id: "bot-id" + name: "bot-name" + , + type: "mention" + text: "User" + mentioned: + id: "user-id" + name: "user-name" + ] + attachments: [] + sourceEvent: + tenant: + id: "tenant-id" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: "bot-id" + user: + id: "user-id" + name: "user-name" + aadObjectId: 'eight888-four-4444-fore-twelve121212' + userPrincipalName: 'em@ai.l' + + afterEach -> + rewiremock.disable() it 'should allow messages without tenant id when tenant filter is empty', -> # Setup delete event.sourceEvent - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expect(receivable).to.be.a('Object') + expect(result).to.be.a('Object') it 'should allow messages with tenant id when tenant filter is empty', -> # Setup - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expect(receivable).to.be.a('Object') + expect(result).to.be.a('Object') it 'should allow messages from allowed tenant ids', -> # Setup process.env.HUBOT_OFFICE365_TENANT_FILTER = event.sourceEvent.tenant.id - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expect(receivable).to.be.a('Object') + expect(result).to.be.a('Object') it 'should block messages from unallowed tenant ids', -> # Setup process.env.HUBOT_OFFICE365_TENANT_FILTER = event.sourceEvent.tenant.id event.sourceEvent.tenant.id = "different-tenant-id" - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expect(receivable).to.be.null + expect(result).to.be.null it 'return generic message when appropriate type is not found', -> # Setup event.type = 'typing' - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expect(receivable).to.be.not.null + expect(result).to.be.not.null + + # Test when message is from follow up button + it 'should construct query when activity value is defined (message from button click)', -> + # Setup + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + delete event.text + prefix = "gho add (members|repos) to team " + event.value = { + "queryPrefix": prefix + "#{prefix} - query0": "hubot gho add " + "#{prefix} - query1": " " + "#{prefix} - query2": " to team " + "#{prefix} - input0": "members" + "#{prefix} - input1": "a-member" + "#{prefix} - input2": "some-team" + } + + # Action + result = null + expect(() -> + result = teamsMiddleware.toReceivable(event, chatMembers) + ).to.not.throw() + + # Assert + expect(result).to.be.a('Object') + expect(result.text).to.eql "#{robot.name} gho add members a-member to team some-team" it 'should work when activity text is an object', -> # Setup event.text = event - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expect(receivable.text).to.equal(event) + expect(result.text).to.equal(event) it 'should work when mentions not provided', -> # Setup delete event.entities - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expect(receivable.text).to.equal(event.text) + expect(result.text).to.equal(event.text) - - it 'should replace all @ mentions', -> + it 'should replace all @ mentions to the bot with the bot name', -> # Setup - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expected = "#{robot.name} do something #{robot.name} and tell user-name about it" - expect(receivable.text).to.equal(expected) + expected = "#{robot.name} do something #{robot.name} and tell \ + #{event.address.user.userPrincipalName} about it" + expect(result.text).to.equal(expected) + + it 'should replace all @ mentions to chat users with their user principal name', -> + # Setup + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + event.text = 'Bot do something Bot and tell User \ + and User2 about it' + event.entities.push( + type: "mention" + text: "User2" + mentioned: + id: 'user2-id' + name: 'user2 two' + ) - it 'should replace at mentions even when entities is not an array', -> + # Action + result = null + expect(() -> + result = teamsMiddleware.toReceivable(event, chatMembers) + ).to.not.throw() + + # Assert + expected = "#{robot.name} do something #{robot.name} and tell \ + #{event.address.user.userPrincipalName} and em@ai.l2 about it" + expect(result.text).to.equal(expected) + + it 'should replace @ mentions even when entities is not an array', -> # Setup event.entities = event.entities[0] - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + event.text = "Bot do something Bot" + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expected = "#{robot.name} do something #{robot.name} and tell User about it" - expect(receivable.text).to.equal(expected) - - it 'should prepend bot name in 1:1 chats', -> + expected = "#{robot.name} do something #{robot.name}" + expect(result.text).to.equal(expected) + + it 'should replace @ mentions to non-chat roster users with their name', -> # Setup - event.address.conversation.id = event.address.user.id - event.text = 'do something Bot and tell User about it' - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + event.entities[1] = + type: "mention" + text: "User" + mentioned: + id: "not-a-valid-user-id" + name: "not-a-user" # Action - receivable = null + result = null expect(() -> - receivable = teamsMiddleware.toReceivable(event) + result = teamsMiddleware.toReceivable(event, chatMembers) ).to.not.throw() # Assert - expected = "#{robot.name} do something #{robot.name} and tell user-name about it" - expect(receivable.text).to.equal(expected) + expected = "#{robot.name} do something #{robot.name} and tell \ + #{event.entities[1].mentioned.name} about it" + expect(result.text).to.equal(expected) + + it 'should trim whitespace before and after text', -> + # Setup + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + event.text = " #{event.text} \n " + + # Action + result = null + expect(() -> + result = teamsMiddleware.toReceivable(event, chatMembers) + ).to.not.throw() + + # Assert + expected = "#{robot.name} do something #{robot.name} and tell \ + #{event.address.user.userPrincipalName} about it" + expect(result.text).to.equal(expected) + + it 'should prepend bot name in 1:1 chats when name is not there', -> + # Setup + event.address.conversation.conversationType = 'personal' + delete event.address.conversation.isGroup + event.text = 'do something Bot and tell User about it' + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + + # Action + result = null + expect(() -> + result = teamsMiddleware.toReceivable(event, chatMembers) + ).to.not.throw() + + # Assert + expected = "#{robot.name} do something #{robot.name} and tell \ + #{event.address.user.userPrincipalName} about it" + expect(result.text).to.equal(expected) describe 'toSendable', -> robot = null message = null context = null + appId = 'a-app-id' + appPassword = 'a-app-password' + beforeEach -> robot = new MockRobot context = @@ -228,7 +464,7 @@ describe 'MicrosoftTeamsMiddleware', -> it 'should create message object for string messages', -> # Setup - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action sendable = null @@ -253,7 +489,7 @@ describe 'MicrosoftTeamsMiddleware', -> # Setup message = type: "some message type" - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action sendable = null @@ -271,6 +507,115 @@ describe 'MicrosoftTeamsMiddleware', -> expect(sendable).to.deep.equal(expected) + # Should construct response card for specific queries + it 'should construct response card for specific queries', -> + # Setup + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + context.user.activity.text = 'hubot gho list teams' + + # Action + sendable = null + expect(() -> + sendable = teamsMiddleware.toSendable(context, message) + ).to.not.throw() + + # Verify + expectedResponseCard = { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "#{context.user.activity.text}" + 'speak': "#{context.user.activity.text}" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': "#{message}" + 'speak': "#{message}" + } + ] + "actions": [ + { + "title": "gho list" + "type": "Action.ShowCard" + "card": { + "type": "AdaptiveCard" + "body": [ + { + 'type': 'TextBlock' + 'text': "gho list" + 'speak': "gho list" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': 'List what?' + 'speak': "List what?" + }, + { + "type": "Input.ChoiceSet" + "id": "gho list (teams|repos|members) - input0" + "style": "compact" + "value": "teams" + "choices": [ + { + "title": "teams" + "value": "teams" + }, + { + "title": "repos" + "value": "repos" + }, + { + "title": "members" + "value": "members" + } + ] + } + ], + 'actions': [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + 'queryPrefix': "gho list (teams|repos|members)" + "gho list (teams|repos|members) - query0": 'hubot gho list ' + } + } + ] + } + }, + { + 'type': 'Action.Submit' + 'title': 'gho list public repos' + 'data': { + 'queryPrefix': "gho list public repos" + "gho list public repos - query0": 'hubot gho list public repos' + } + } + ] + } + } + expected = [ + type: 'typing', + address: context.user.activity.address + , + type: 'message' + address: context.user.activity.address + attachments: [ + expectedResponseCard + ] + ] + + expect(sendable).to.deep.equal(expected) + it 'should convert slack @ mentions with only id', -> # Setup robot.brain.users = () -> @@ -279,7 +624,7 @@ describe 'MicrosoftTeamsMiddleware', -> name:'user' message = "<@1234> Hello! <@1234>" - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action sendable = null @@ -320,7 +665,7 @@ describe 'MicrosoftTeamsMiddleware', -> name:'user' message = "<@1234|mention text> Hello! <@1234|different>" - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action sendable = null @@ -356,7 +701,7 @@ describe 'MicrosoftTeamsMiddleware', -> it 'should convert slack @ mentions with unfound user', -> # Setup message = "<@1234> Hello! <@1234|different>" - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action sendable = null @@ -392,7 +737,7 @@ describe 'MicrosoftTeamsMiddleware', -> it 'should convert images', -> # Setup message = "http://test.com/thisisanimage.jpg" - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action sendable = null @@ -419,7 +764,7 @@ describe 'MicrosoftTeamsMiddleware', -> it 'should not convert other links', -> # Setup message = "http://test.com/thisisanimage.html" - teamsMiddleware = new MicrosoftTeamsMiddleware(robot) + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) # Action sendable = null @@ -438,4 +783,1120 @@ describe 'MicrosoftTeamsMiddleware', -> address: context.user.activity.address ] - expect(sendable).to.deep.equal(expected) \ No newline at end of file + expect(sendable).to.deep.equal(expected) + + it "should escape < when message starts with 'hubot' (such as for hubot help)", -> + # Setup + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + message = "hubot command - this message has < symbols in multiple places <" + + # Action + sendable = null + expect(() -> + sendable = teamsMiddleware.toSendable(context, message) + ).to.not.throw() + + # Verify + expected = [ + type: 'typing', + address: context.user.activity.address + , + type: 'message' + entities: [] + text: "hubot command <blah> - this message has < symbols in multiple places <" + address: context.user.activity.address + ] + + expect(sendable).to.deep.equal(expected) + + it "should replace \\n with
in text to render correctly in Teams", -> + # Setup + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + message = "some \nmessage" + + # Action + sendable = null + expect(() -> + sendable = teamsMiddleware.toSendable(context, message) + ).to.not.throw() + + # Verify + expected = [ + type: 'typing', + address: context.user.activity.address + , + type: 'message' + entities: [] + text: "some
message" + address: context.user.activity.address + ] + + expect(sendable).to.deep.equal(expected) + + describe 'maybeReceive', -> + rewiremock('botbuilder-teams').with(require('./mock-botbuilder-teams')) + BotBuilderTeams = null + robot = null + teamsMiddleware = null + connector = null + authEnabled = true + event = null + appId = 'a-app-id' + appPassword = 'a-app-password' + + beforeEach -> + rewiremock.enable() + MicrosoftTeamsMiddleware = require '../src/msteams-middleware' + BotBuilderTeams = require 'botbuilder-teams' + + robot = new MockRobot + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + connector = new BotBuilderTeams.TeamsChatConnector({ + appId: 'a-app-id' + appPassword: 'a-app-password' + }) + connector.send = (payload, cb) -> + robot.brain.set("payload", payload) + authEnabled = true + + event = + type: 'message' + text: 'Bot do something Bot and tell User about it' + agent: 'tests' + source: 'msteams' + entities: [ + type: "mention" + text: "Bot" + mentioned: + id: "bot-id" + name: "bot-name" + , + type: "mention" + text: "User" + mentioned: + id: "user-id" + name: "user-name" + ] + attachments: [] + sourceEvent: + tenant: + id: "tenant-id" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: "bot-id" + user: + id: "user-id" + name: "user-name" + aadObjectId: 'eight888-four-4444-fore-twelve121212' + userPrincipalName: 'em@ai.l' + + afterEach -> + rewiremock.disable() + + # Hubot receives message when auth disabled + it 'hubot receives message when auth is disabled', -> + # Setup + authEnabled = false + + # Action + expect(() -> + teamsMiddleware.maybeReceive(event, connector, authEnabled) + ).to.not.throw() + + # Assert + resultEvent = robot.brain.get("event") + expect(resultEvent).to.not.be.null + expect(resultEvent).to.be.a('Object') + + # Hubot receives message when auth enabled and user is authorized + it 'hubot receives message when auth is enabled and user is authorized', -> + # Setup + robot.brain.set("authorizedUsers", { + 'an-1_20@em.ail': true + 'em@ai.l': false + 'user-UPN': true + }) + + # Action + expect(() -> + teamsMiddleware.maybeReceive(event, connector, authEnabled) + ).to.not.throw() + + # Assert + resultEvent = robot.brain.get("event") + expect(resultEvent).to.not.be.null + expect(resultEvent).to.be.a('Object') + + # Hubot sends error resposne when auth enabled and user is not authorized + it 'hubot sends error response when auth is enabled and user is not authorized', (done) -> + # Setup + robot.brain.set("authorizedUsers", { + 'an-1_20@em.ail': true + 'user-UPN': true + }) + expected = [{ + type: 'typing' + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: "bot-id" + user: + id: "user-id" + name: "user-name" + aadObjectId: 'eight888-four-4444-fore-twelve121212' + userPrincipalName: 'em@ai.l' + }, + { + type: 'message' + text: "You are not authorized to send commands to hubot. To gain access, \ + talk to your admins:\r\n- an-1_20@em.ail\r\n- user-UPN" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: "bot-id" + user: + id: "user-id" + name: "user-name" + aadObjectId: 'eight888-four-4444-fore-twelve121212' + userPrincipalName: 'em@ai.l' + }] + + # Action + expect(() -> + teamsMiddleware.maybeReceive(event, connector, authEnabled) + ).to.not.throw() + + # Assert + setTimeout((robot, expected) -> + result = robot.brain.get("payload") + expect(result).to.not.be.null + expect(result).to.be.a('Array') + expect(result).to.deep.eql(expected) + done() + , 200, robot, expected) + + describe 'send', -> + rewiremock('botbuilder-teams').with(require('./mock-botbuilder-teams')) + BotBuilderTeams = null + robot = null + teamsMiddleware = null + connector = null + payload = null + appId = 'a-app-id' + appPassword = 'a-app-password' + cb = () -> {} + + beforeEach -> + rewiremock.enable() + MicrosoftTeamsMiddleware = require '../src/msteams-middleware' + BotBuilderTeams = require 'botbuilder-teams' + + robot = new MockRobot + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + connector = new BotBuilderTeams.TeamsChatConnector({ + appId: 'a-app-id' + appPassword: 'a-app-password' + }) + connector.send = (payload, cb) -> + robot.brain.set("payload", payload) + + payload = [{ + type: 'typing' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + }, + { + type: 'message' + text: "a message" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + aadObjectId: "user-aad-id" + userPrincipalName: "user-UPN" + }] + + afterEach -> + rewiremock.disable() + + it 'should set justReceivedResponse on first message received and store response', -> + # Setup + + # Action + expect(() -> + teamsMiddleware.send(connector, payload) + ).to.not.throw() + + # Assert + expect(robot.brain.get("justReceivedResponse")).to.be.true + expect(robot.brain.get("teamsResponse")).to.not.be.null + + it 'should send correct response when one response is sent', (done) -> + # Setup + expected = [{ + type: 'typing' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + }, + { + type: 'message' + text: "a message" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + aadObjectId: "user-aad-id" + userPrincipalName: "user-UPN" + }] + + # Action + expect(() -> + teamsMiddleware.send(connector, payload) + ).to.not.throw() + + # Assert, after 200ms to allow the payload to be "sent" + setTimeout((robot, expected) -> + result = robot.brain.get("payload") + expect(result).to.deep.eql(expected) + done() + , 200, robot, expected) + + it 'should send combined responses for messages received within 100ms', -> + # Setup + payload2 = [{ + type: 'typing' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + }, + { + type: 'message' + text: "another message" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + aadObjectId: "user-aad-id" + userPrincipalName: "user-UPN" + }] + expected = [{ + type: 'typing' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + }, + { + type: 'message' + text: "a message\nanother message" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + aadObjectId: "user-aad-id" + userPrincipalName: "user-UPN" + }] + + # Action + expect(() -> + teamsMiddleware.send(connector, payload) + ).to.not.throw() + expect(() -> + teamsMiddleware.send(connector, payload2) + ).to.not.throw() + + # Assert, after 200ms to allow the payload to be "sent" + setTimeout((robot, expected) -> + result = robot.brain.get("payload") + expect(result).to.deep.eql(expected) + done() + , 200, robot, expected) + + describe 'sendPayload', -> + rewiremock('botbuilder-teams').with(require('./mock-botbuilder-teams')) + BotBuilderTeams = null + robot = null + teamsMiddleware = null + connector = null + payload = null + appId = 'a-app-id' + appPassword = 'a-app-password' + cb = () -> {} + + beforeEach -> + rewiremock.enable() + MicrosoftTeamsMiddleware = require '../src/msteams-middleware' + BotBuilderTeams = require 'botbuilder-teams' + + robot = new MockRobot + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + connector = new BotBuilderTeams.TeamsChatConnector({ + appId: 'a-app-id' + appPassword: 'a-app-password' + }) + connector.send = (payload, cb) -> + robot.brain.set("payload", payload) + + payload = { + type: 'message' + text: "" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + aadObjectId: "user-aad-id" + userPrincipalName: "user-UPN" + } + + afterEach -> + rewiremock.disable() + + it 'should package non-array payload in array before sending', -> + # Setup + expected = [{ + type: 'message' + text: "" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + aadObjectId: "user-aad-id" + userPrincipalName: "user-UPN" + }] + + # Action + expect(() -> + teamsMiddleware.sendPayload(connector, payload) + ).to.not.throw() + + # Assert + result = robot.brain.get("payload") + expect(result).to.deep.eql(expected) + + it 'should pass payload array through unchanged', -> + # Setup + payload = [payload] + expected = [{ + type: 'message' + text: "" + address: + conversation: + isGroup: 'true' + conversationType: 'channel' + id: "19:conversation-id" + bot: + id: 'a-app-id' + user: + id: "user-id" + name: "user-name" + aadObjectId: "user-aad-id" + userPrincipalName: "user-UPN" + }] + + # Action + expect(() -> + teamsMiddleware.sendPayload(connector, payload) + ).to.not.throw() + + # Assert + result = robot.brain.get("payload") + expect(result).to.deep.eql(expected) + + describe 'combineResponses', -> + robot = null + teamsMiddleware = null + storedPayload = null + newPayload = null + expected = null + appId = 'a-app-id' + appPassword = 'a-app-password' + + beforeEach -> + robot = new MockRobot + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + storedPayload = [ + { + type: 'typing' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + }, + { + type: 'message' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + entities: [ + { + field: 'some entity' + } + ] + } + ] + newPayload = [ + { + type: 'typing' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + }, + { + type: 'message' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + entities: [ + { + field: 'another entitiy' + } + ] + } + ] + expected = [ + { + type: 'typing' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + }, + { + type: 'message' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + entities: [ + { + field: 'some entity' + } + ] + } + ] + + it 'should not change stored payload text when both stored and new payload text is undefined', -> + # Setup + + # Action + expect(() -> + teamsMiddleware.combineResponses(storedPayload, newPayload) + ).to.not.throw() + + # Assert + expect(storedPayload).to.deep.equal(expected) + + it 'should not change stored payload text when new payload text is undefined', -> + # Setup + storedPayload[1].text = 'this is some stored text' + expected[1].text = 'this is some stored text' + + # Action + expect(() -> + teamsMiddleware.combineResponses(storedPayload, newPayload) + ).to.not.throw() + + # Assert + expect(storedPayload).to.deep.equal(expected) + + it 'should add new payload text when stored payload text is undefined', -> + # Setup + newPayload[1].text = 'new payload' + expected[1].text = 'new payload' + + # Action + expect(() -> + teamsMiddleware.combineResponses(storedPayload, newPayload) + ).to.not.throw() + + # Assert + expect(storedPayload).to.deep.equal(expected) + + it 'should combine both payload texts when both have text', -> + # Setup + storedPayload[1].text = 'this is some stored text' + newPayload[1].text = 'new payload' + expected[1].text = "this is some stored text\r\nnew payload" + + # Action + expect(() -> + teamsMiddleware.combineResponses(storedPayload, newPayload) + ).to.not.throw() + + # Assert + expect(storedPayload).to.deep.equal(expected) + + it 'should not change stored payload attachments when both stored and new don\'t have attachments', -> + # Setup + + # Action + expect(() -> + teamsMiddleware.combineResponses(storedPayload, newPayload) + ).to.not.throw() + + # Assert + expect(storedPayload).to.deep.equal(expected) + + it 'should not change stored payload attachments when new doesn\'t have attachments', -> + # Setup + storedPayload[1].attachments = [ + { + contentType: 'image' + url: 'some-image-url' + } + ] + expected[1].attachments = [ + { + contentType: 'image' + url: 'some-image-url' + } + ] + + # Action + expect(() -> + teamsMiddleware.combineResponses(storedPayload, newPayload) + ).to.not.throw() + + # Assert + expect(storedPayload).to.deep.equal(expected) + + # stored doesn't have, set stored to equal new attachments + it 'should add all attachments to stored when stored doesn\'t have attachments and new does', -> + # Setup + newPayload[1].attachments = [ + { + contentType: 'image' + url: 'some-image-url' + } + ] + expected[1].attachments = [ + { + contentType: 'image' + url: 'some-image-url' + } + ] + + # Action + expect(() -> + teamsMiddleware.combineResponses(storedPayload, newPayload) + ).to.not.throw() + + # Assert + expect(storedPayload).to.deep.equal(expected) + + # both have but stored doesn't have adaptive card, append new attachments + # stored doesn't have, set stored to equal new attachments + it 'should append all new attachments when stored doesn\'t have adaptive card attachment', -> + # Setup + storedPayload[1].attachments = [ + { + contentType: 'image' + url: 'some-image-url' + } + ] + newPayload[1].attachments = [ + { + contentType: 'image' + url: 'another-image-url' + }, + { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "Some text" + 'speak': "Some text" + 'weight': 'bolder' + 'size': 'large' + } + ] + } + } + ] + expected[1].attachments = [ + { + contentType: 'image' + url: 'some-image-url' + }, + { + contentType: 'image' + url: 'another-image-url' + }, + { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "Some text" + 'speak': "Some text" + 'weight': 'bolder' + 'size': 'large' + } + ] + } + } + ] + + # Action + expect(() -> + teamsMiddleware.combineResponses(storedPayload, newPayload) + ).to.not.throw() + + # Assert + expect(storedPayload).to.deep.equal(expected) + + it 'should combine attachments correctly so there is only one adaptive card attachment in the end', -> + # Setup + storedPayload[1].attachments = [ + { + contentType: 'image' + url: 'some-image-url' + }, + { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "Some text" + 'speak': "Some text" + 'weight': 'bolder' + 'size': 'large' + } + ] + } + } + ] + newPayload[1].attachments = [ + { + contentType: 'image' + url: 'another-image-url' + }, + { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "Some more text" + 'speak': "Some more text" + 'weight': 'bolder' + 'size': 'large' + } + ] + } + } + ] + expected[1].attachments = [ + { + contentType: 'image' + url: 'some-image-url' + }, + { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "Some text" + 'speak': "Some text" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': "Some more text" + 'speak': "Some more text" + 'weight': 'bolder' + 'size': 'large' + } + ] + } + }, + { + contentType: 'image' + url: 'another-image-url' + } + ] + + # Action + expect(() -> + teamsMiddleware.combineResponses(storedPayload, newPayload) + ).to.not.throw() + + # Assert + expect(storedPayload).to.deep.equal(expected) + + describe 'constructErrorResponse', -> + robot = null + teamsMiddleware = null + activity = null + text = null + appendAdmins = false + expected = null + appId = 'a-app-id' + appPassword = 'a-app-password' + + beforeEach -> + robot = new MockRobot + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + activity = + address: + addressField: "a value" + anotherProperty: "something else" + text = "This text will be displayed to the user" + appendAdmins = false + expected = [ + { + type: 'typing' + address: + addressField: "a value" + anotherProperty: "something else" + }, + { + type: 'message' + text: "#{text}" + address: + addressField: "a value" + anotherProperty: "something else" + } + ] + + it 'should return a proper payload with the error text', -> + # Setup + + # Action + payload = null + expect(() -> + payload = teamsMiddleware.constructErrorResponse(activity, text, appendAdmins) + ).to.not.throw() + + # Assert + expect(payload).to.deep.equal(expected) + + it 'should include admins in the payload message text when requested', -> + # Setup + appendAdmins = true + robot.brain.set("authorizedUsers", { + "user0@some.upn": false + "user1@website.place": true + "user2@someother.upn": false + "user3@another.site": true + }) + expected[1].text = "#{expected[1].text}\r\n- user1@website.place\r\n- user3@another.site" + + # Action + payload = null + expect(() -> + payload = teamsMiddleware.constructErrorResponse(activity, text, appendAdmins) + ).to.not.throw() + + # Assert + expect(payload).to.deep.equal(expected) + + describe 'maybeConstructUserInputPrompt', -> + robot = null + teamsMiddleware = null + event = null + expected = null + appId = 'a-app-id' + appPassword = 'a-app-password' + + beforeEach -> + robot = new MockRobot + teamsMiddleware = new MicrosoftTeamsMiddleware(robot, appId, appPassword) + event = + value: + hubotMessage: 'hubot gho delete team ' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + expected = [ + { + type: 'typing' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + }, + { + type: 'message' + address: + id: 'address-id' + channelId: 'channel-id' + user: + id: 'user-id' + name: 'user-name' + aadObjectId: 'user-aadobject-id' + userPrincipalName: "user-UPN" + conversation: + conversationType: 'personal' + id: 'conversation-id' + bot: + id: 'bot-id' + name: 'bot-name' + serviceUrl: 'service-url' + attachments: [ + { + 'contentType': 'application/vnd.microsoft.card.adaptive' + 'content': { + "type": "AdaptiveCard" + "version": "1.0" + "body": [ + { + 'type': 'TextBlock' + 'text': "gho delete team" + 'speak': "gho delete team" + 'weight': 'bolder' + 'size': 'large' + }, + { + 'type': 'TextBlock' + 'text': "What is the name of the team to delete? (Max 1024 characters)" + 'speak': "What is the name of the team to delete? (Max 1024 characters)" + }, + { + 'type': 'Input.Text' + 'id': "gho delete team - input0" + 'speak': "What is the name of the team to delete? (Max 1024 characters)" + 'wrap': true + 'style': 'text' + 'maxLength': 1024 + } + ] + "actions": [ + { + 'type': 'Action.Submit' + 'title': 'Submit' + 'speak': 'Submit' + 'data': { + 'queryPrefix': 'gho delete team ' + 'gho delete team - query0': 'hubot gho delete team ' + } + } + ] + } + } + ] + } + ] + + # Should construct a payload containing a user input card for specific queries + it 'should construct payload containing user input card for specific queries', -> + # Setup + + # Action + result = null + expect(() -> + result = teamsMiddleware.maybeConstructUserInputPrompt(event) + ).to.not.throw() + + # Assert + expect(result).to.deep.equal(expected) + + # Should return null for queries other than those that should return a payload + it 'should return null for queries that don\'t need an input card', -> + # Setup + event.value.hubotMessage = 'hubot gho' + + # Action and Assert + expect(teamsMiddleware.maybeConstructUserInputPrompt(event)).to.be.null