├── demo ├── action.json ├── index.js └── local.js ├── lib ├── directline │ ├── model │ │ └── AttachmentTypes.json │ ├── DirectLineClientStorage.js │ ├── DirectLineManager.js │ └── DirectLineWrapper.js ├── actions-on-google │ ├── model │ │ ├── Attachment.js │ │ ├── Instructions.js │ │ └── Activity.js │ ├── InstructionBuilder.js │ └── MessageBuilder.js ├── queue │ └── GoogleOutboundQueue.js └── fulfilment.js ├── test ├── directline │ ├── DirectLineClientStorageTest.js │ └── DirectLineWrapperTest.js ├── actions-on-google │ ├── MessageBuilderTest.js │ └── MessageBuilder2Test.js └── queue │ └── GoogleOutboundQueueTest.js ├── package.json ├── index.js ├── LICENSE ├── .gitignore ├── .circleci └── config.yml ├── README.md └── yarn.lock /demo/action.json: -------------------------------------------------------------------------------- 1 | {"actions":[{"description":"Default Welcome Intent","name":"MAIN","fulfillment":{"conversationName":"MAIN_CONVERSATION"},"intent":{"name":"actions.intent.MAIN","trigger":{"queryPatterns":["talk to Bot Framework"]}}}],"conversations":{"MAIN_CONVERSATION":{"name":"MAIN_CONVERSATION","url":"https://41aa9e69.ngrok.io"}}} -------------------------------------------------------------------------------- /lib/directline/model/AttachmentTypes.json: -------------------------------------------------------------------------------- 1 | { 2 | "AnimationCard": "application/vnd.microsoft.card.animation", 3 | "AudioCard": "application/vnd.microsoft.card.audio", 4 | "HeroCard": "application/vnd.microsoft.card.hero", 5 | "ReceiptCard": "application/vnd.microsoft.com.card.receipt", 6 | "SigninCard": "application/vnd.microsoft.card.signin", 7 | "ThumbnailCard": "application/vnd.microsoft.card.thumbnail", 8 | "VideoCard": "application/vnd.microsoft.card.video" 9 | } -------------------------------------------------------------------------------- /lib/directline/DirectLineClientStorage.js: -------------------------------------------------------------------------------- 1 | class DirectLineClientStorage { 2 | constructor() { 3 | this._clients = new Map(); 4 | } 5 | getClient(conversationId) { 6 | return this._clients.get(conversationId); 7 | } 8 | storeClient(conversationId, directLineWrapper) { 9 | return this._clients.set(conversationId, directLineWrapper); 10 | } 11 | removeClient(conversationId) { 12 | return this._clients.delete(conversationId); 13 | } 14 | } 15 | module.exports = new DirectLineClientStorage(); -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | //========================================================= 2 | // Import NPM modules 3 | //========================================================= 4 | const express = require("express"); 5 | const actionsOnGoogleAdapter = require("../")(process.env.DIRECT_LINE_SECRET); 6 | 7 | const PORT = process.env.PORT || 3000; 8 | const app = express(); 9 | 10 | app.use(actionsOnGoogleAdapter.router); 11 | 12 | actionsOnGoogleAdapter.onUserSignedInHandlerProvider.registerHandler((user) => { 13 | //Get additional user details from your API once account is linked and access token is available 14 | // return fetch('https://your-api/user/' + user.userId, { 15 | // headers: { 16 | // Authorization: `Bearer ${user.accessToken}` 17 | // } 18 | // }).then(res => res.json()) 19 | return Promise.resolve({username: 'John87', ...user}) 20 | }); 21 | 22 | app.listen(PORT, () => console.log(`ActionsOnGoogle demo listening on port ${PORT}!`)); -------------------------------------------------------------------------------- /test/directline/DirectLineClientStorageTest.js: -------------------------------------------------------------------------------- 1 | const directLineClientStorage = require("../../lib/directline/DirectLineClientStorage"); 2 | const assert = require('assert'); 3 | 4 | describe('DirectLineClientStorage', () => { 5 | it('should return 123 when the value 123 is stored against key "test"', () => { 6 | directLineClientStorage.storeClient("test", 123); 7 | assert.equal(directLineClientStorage.getClient("test"), 123); 8 | }); 9 | it('should be able to store an object against a given key', () => { 10 | let objectToBeStored = { 11 | number: new Number() 12 | }; 13 | directLineClientStorage.storeClient("object-lives-here", objectToBeStored); 14 | assert.equal(directLineClientStorage.getClient("object-lives-here"), objectToBeStored); 15 | }); 16 | it('should return undefined when no value is stored for key', () => { 17 | assert.equal(directLineClientStorage.getClient("should-be-undefined")); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-framework-actions-on-google", 3 | "description": "This NPM Module adds a Microsoft Bot Framework integration to Actions on Google - turning it into an input channel.", 4 | "version": "2.3.1", 5 | "main": "index.js", 6 | "repository": "git@github.com:Capgemini-AIE/bot-framework-actions-on-google.git", 7 | "author": "Dan Cotton", 8 | "license": "MIT", 9 | "dependencies": { 10 | "actions-on-google": "^1.7.0", 11 | "body-parser": "^1.18.2", 12 | "botframework-directlinejs": "^0.9.14", 13 | "express": "^4.16.2", 14 | "node-fetch": "^1.7.3", 15 | "xhr2": "^0.1.4" 16 | }, 17 | "devDependencies": { 18 | "dotenv": "^4.0.0", 19 | "mocha": "^4.1.0", 20 | "mocha-circleci-reporter": "^0.0.2", 21 | "ngrok": "^2.2.24", 22 | "npm-cli-login": "^0.0.10", 23 | "sinon": "^4.1.3" 24 | }, 25 | "scripts": { 26 | "start": "pushd demo; node local; popd;", 27 | "test": "mocha --reporter mocha-circleci-reporter test/**/*.js", 28 | "login-cli": "npm-cli-login" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | //========================================================= 2 | // Import NPM modules 3 | //========================================================= 4 | const express = require("express"); 5 | const bodyParser = require("body-parser"); 6 | const fulfill = require("./lib/fulfilment"); 7 | 8 | class OnUserSignedInHandlerProvider { 9 | constructor() { 10 | this.handler = (user) => { 11 | return Promise.resolve(user) 12 | } 13 | } 14 | 15 | registerHandler(handler) { 16 | this.handler = handler 17 | } 18 | 19 | getHandler() { 20 | return this.handler 21 | } 22 | } 23 | 24 | 25 | module.exports = (directlineSecret, conversationTimeout, shouldGetUsersNameFromGoogle = true) => { 26 | const onUserSignedInHandlerProvider = new OnUserSignedInHandlerProvider(); 27 | const router = express.Router(); 28 | router.use(bodyParser.json()); 29 | router.use(fulfill(directlineSecret, onUserSignedInHandlerProvider, conversationTimeout, shouldGetUsersNameFromGoogle)); 30 | 31 | return { 32 | onUserSignedInHandlerProvider, 33 | router 34 | }; 35 | }; -------------------------------------------------------------------------------- /demo/local.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { exec } = require('child_process'); 3 | const ngrok = require('ngrok'); 4 | const fs = require('fs'); 5 | 6 | const actionMeta = Object.assign({}, require("./action.json")); 7 | const app = require("./index.js"); 8 | 9 | ngrok.connect(process.env.PORT || 3000, (err, url) => { 10 | console.log("[NGROK] Obtained URL: " + url); 11 | // Write to actions metadata file 12 | actionMeta.conversations.MAIN_CONVERSATION.url = url; 13 | fs.writeFile("./action.json", JSON.stringify(actionMeta), (err) => { 14 | if (err) { 15 | return console.error(err); 16 | } 17 | console.log("[FS] Rewrote actions.json to update URL"); 18 | // Update GACTIONS for local dev. 19 | exec(`gactions update --action_package action.json --project ${process.env.GACTIONS_PROJECT_ID}`, (err, stdout, stderr) => { 20 | if (err) { 21 | // Failed 22 | return console.error(err); 23 | } 24 | console.log("[GACTIONS] Successfully executed update via gactions CLI"); 25 | // Done. Leave app running. 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/actions-on-google/model/Attachment.js: -------------------------------------------------------------------------------- 1 | const AttachmentTypes = require("../../directline/model/AttachmentTypes.json"); 2 | const Instructions = require("./Instructions"); 3 | 4 | module.exports = class Activity { 5 | 6 | constructor(attachment) { 7 | switch (attachment.contentType) { 8 | case AttachmentTypes.AudioCard: 9 | this.getGoogleActionsInstructions = () => this.getAudioCardInstructions(attachment); 10 | break; 11 | case AttachmentTypes.SigninCard: 12 | this.getGoogleActionsInstructions = () => this.getSignInCardInstructions(attachment); 13 | break; 14 | default: 15 | this.getGoogleActionsInstructions = () => this.getEmptyInstructions(); 16 | break; 17 | } 18 | } 19 | 20 | getEmptyInstructions() { 21 | return new Instructions(); 22 | } 23 | 24 | getAudioCardInstructions(attachment) { 25 | return new Instructions().appendText(`${attachment.content.media.map(item => ``).join(" ")}`); 26 | } 27 | 28 | getSignInCardInstructions() { 29 | return new Instructions().mergeSignInRequired(true); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Capgemini AIE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/actions-on-google/InstructionBuilder.js: -------------------------------------------------------------------------------- 1 | const Activity = require("./model/Activity"); 2 | const Instructions = require("./model/Instructions"); 3 | 4 | module.exports = class MessageBuilder { 5 | 6 | constructInstructions(activities) { 7 | const startingInstructions = new Instructions(); 8 | const instructions = activities.reduce((accumulatedInstructions, activity) => { 9 | const instructions = new Activity(activity).getGoogleActionsInstructions(); 10 | return accumulatedInstructions.mergeInstructions(instructions, true); 11 | }, startingInstructions); 12 | return instructions.getInstructions() 13 | } 14 | 15 | actOnInstructions(instructions, app, state) { 16 | console.log(`User: ${JSON.stringify(app.getUser())}`); 17 | if (instructions.signInRequired) { 18 | console.log(`Asking for sign in`); 19 | app.askForSignIn(); 20 | } else { 21 | if(instructions.shouldEndSession) 22 | { 23 | app.tell(`${instructions.text}`, state) 24 | } 25 | else 26 | { 27 | app.ask(`${instructions.text}`, state) 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Test Results 15 | test-results.xml 16 | 17 | # env 18 | .env 19 | 20 | # Creds 21 | creds.data 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (http://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Typescript v1 declaration files 49 | typings/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | 69 | .idea 70 | 71 | -------------------------------------------------------------------------------- /lib/queue/GoogleOutboundQueue.js: -------------------------------------------------------------------------------- 1 | class GoogleOutboundQueue { 2 | constructor() { 3 | this._queue = new Map(); 4 | this._interval = setInterval(this.queueWorker.bind(this), process.env.QUEUE_GROUPING_TIMEOUT || 800) 5 | } 6 | queueWorker() { 7 | Array.from(this._queue.keys()) 8 | .map(key => ({ 9 | key, 10 | value: Object.assign({}, this._queue.get(key)) 11 | })) 12 | .map((pair) => { 13 | let { key, value } = pair; 14 | if (value.count > 0 && value.count === value.messages.length) { 15 | let activities = value.messages; 16 | console.log("Sending message back for conv id: " + key, activities); 17 | value.messages[value.count - 1].ask(activities); 18 | value.messages = []; 19 | value.count = 0; 20 | } else { 21 | // console.log(`[QUEUE] ${value.count} !== ${value.messages.length} for ${key}`); 22 | value.count = value.messages.length; 23 | } 24 | this._queue.set(key, value); 25 | }) 26 | } 27 | enqueueMessage(conversationId, activity, ask) { 28 | let entry = this._queue.get(conversationId) || {}; 29 | entry.messages = [].concat(entry.messages || [], Object.assign({}, activity, { 30 | ask 31 | })); 32 | return this._queue.set(conversationId, entry); 33 | } 34 | } 35 | module.exports = new GoogleOutboundQueue(); -------------------------------------------------------------------------------- /lib/actions-on-google/model/Instructions.js: -------------------------------------------------------------------------------- 1 | module.exports = class Instructions { 2 | 3 | constructor() { 4 | this.signInRequired = false; 5 | this.text = ''; 6 | this.shouldEndSession = false; 7 | } 8 | 9 | appendText(newText, requiresBreak) { 10 | if (this.text && requiresBreak) { 11 | this.text = `${this.text} . ${newText}` 12 | } else { 13 | if (this.text) { 14 | this.text = `${this.text} ${newText}` 15 | } else { 16 | this.text = newText 17 | } 18 | } 19 | return this; 20 | } 21 | 22 | mergeSignInRequired(signInRequired) { 23 | if (!this.signInRequired) { 24 | this.signInRequired = signInRequired 25 | } 26 | return this; 27 | } 28 | 29 | mergeShouldEndSession(shouldEndSession) { 30 | if (!this.shouldEndSession) { 31 | this.shouldEndSession = shouldEndSession 32 | } 33 | return this; 34 | } 35 | 36 | mergeInstructions(instructions, insertSpeechBreaks) { 37 | this.appendText(instructions.getInstructions().text, insertSpeechBreaks); 38 | this.mergeSignInRequired(instructions.getInstructions().signInRequired); 39 | this.mergeShouldEndSession(instructions.getInstructions().shouldEndSession); 40 | return this; 41 | } 42 | 43 | getInstructions() { 44 | return { 45 | text: this.text, 46 | signInRequired: this.signInRequired, 47 | shouldEndSession: this.shouldEndSession 48 | } 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /lib/actions-on-google/model/Activity.js: -------------------------------------------------------------------------------- 1 | const Attachment = require('./Attachment') 2 | const Instructions = require('./Instructions') 3 | 4 | module.exports = class Activity { 5 | 6 | constructor(data) { 7 | this.activityData = data; 8 | } 9 | 10 | getGoogleActionsInstructions() { 11 | const instructions = new Instructions(); 12 | instructions.appendText(this.activityData.text); 13 | 14 | const defaultLeaveSessionOpen = (process.env.DEFAULT_LEAVE_SESSION_OPEN) 15 | ? (process.env.DEFAULT_LEAVE_SESSION_OPEN == 'true') 16 | : true; 17 | 18 | const expectingInput = ((this.activityData.inputHint) && (this.activityData.inputHint == 'expectingInput')) ? true : false; 19 | const ignoringInput = ((this.activityData.inputHint) && (this.activityData.inputHint == 'ignoringInput')) ? true : false; 20 | 21 | let shouldLeaveSessionOpen = defaultLeaveSessionOpen; 22 | 23 | if(expectingInput) { 24 | shouldLeaveSessionOpen = true; 25 | } else if(ignoringInput) { 26 | shouldLeaveSessionOpen = false; 27 | } 28 | 29 | instructions.mergeShouldEndSession(!shouldLeaveSessionOpen); 30 | 31 | if (this.activityData.attachments && this.activityData.attachments.length > 0) { 32 | return this.activityData.attachments.reduce((accumulatedInstructions, attachment) => { 33 | const instructions = new Attachment(attachment).getGoogleActionsInstructions(); 34 | return accumulatedInstructions.mergeInstructions(instructions, false) 35 | }, instructions); 36 | } else { 37 | return instructions; 38 | } 39 | } 40 | }; -------------------------------------------------------------------------------- /lib/directline/DirectLineManager.js: -------------------------------------------------------------------------------- 1 | global.XMLHttpRequest = require("xhr2"); 2 | 3 | const { DirectLine, ConnectionStatus } = require("botframework-directlinejs"); 4 | const fetch = require("node-fetch"); 5 | const DirectLineWrapper = require("./DirectLineWrapper"); 6 | 7 | const DIRECTLINE_ENDPOINT = "https://directline.botframework.com"; 8 | 9 | class DirectLineManager { 10 | constructor(directlineSecret) { 11 | this._secret = directlineSecret; 12 | } 13 | createConversation() { 14 | return this.getToken(this._secret) 15 | .then(token => new Promise((resolve, reject) => { 16 | console.log("New client with given token: "); 17 | let wrapper = new DirectLineWrapper(new DirectLine({ 18 | token: token, 19 | webSocket: true 20 | })); 21 | wrapper.client.connectionStatus$ 22 | .subscribe(connectionStatus => { 23 | switch(connectionStatus) { 24 | case ConnectionStatus.Online: 25 | console.log("Client ONLINE. "); 26 | resolve(wrapper); 27 | break; 28 | default: 29 | console.log("Conn Status: " + connectionStatus + "" + ConnectionStatus[connectionStatus]); 30 | break; 31 | } 32 | }); 33 | })); 34 | } 35 | getToken(directlineSecret) { 36 | return fetch(`${DIRECTLINE_ENDPOINT}/v3/directline/tokens/generate`, { 37 | method: "POST", 38 | headers: { 39 | Authorization: `Bearer ${directlineSecret}` 40 | } 41 | }) 42 | .then(response => response.json()) 43 | .then(json => console.log(json) || json.token) 44 | } 45 | } 46 | 47 | module.exports = DirectLineManager; -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | test: 8 | docker: 9 | - image: circleci/node:latest 10 | 11 | working_directory: ~/repo 12 | 13 | steps: 14 | - checkout 15 | 16 | # Download and cache dependencies 17 | - restore_cache: 18 | keys: 19 | - v1-dependencies-{{ checksum "package.json" }} 20 | # fallback to using the latest cache if no exact match is found 21 | - v1-dependencies- 22 | 23 | - save_cache: 24 | paths: 25 | - node_modules 26 | key: v1-dependencies-{{ checksum "package.json" }} 27 | 28 | - run: yarn install 29 | - run: mkdir junit 30 | - run: 31 | command: yarn test 32 | environment: 33 | MOCHA_FILE: junit/test-results.xml 34 | when: always 35 | - store_test_results: 36 | path: ~/repo/junit 37 | - store_artifacts: 38 | path: ~/repo/junit 39 | release: 40 | docker: 41 | - image: circleci/node:latest 42 | working_directory: ~/repo 43 | steps: 44 | - checkout 45 | - run: yarn 46 | - run: npm run login-cli 47 | - run: npm publish 48 | 49 | workflows: 50 | version: 2 51 | test_and_release: 52 | jobs: 53 | - test: 54 | filters: 55 | tags: 56 | only: /.*/ 57 | - release: 58 | requires: 59 | - test 60 | filters: 61 | tags: 62 | only: /v[0-9]+(\.[0-9]+)*/ 63 | branches: 64 | ignore: /.*/ -------------------------------------------------------------------------------- /lib/directline/DirectLineWrapper.js: -------------------------------------------------------------------------------- 1 | module.exports = class DirectLineWrapper { 2 | constructor(directlineClient, user) { 3 | this._messagingCallbacks = []; 4 | this._user = user; 5 | this.client = directlineClient; 6 | this.client.activity$ 7 | .filter(activity => activity.type === 'message') 8 | .subscribe(this._activityHandler.bind(this)); 9 | } 10 | _activityHandler(activity) { 11 | this._messagingCallbacks.map(callback => callback(activity)); 12 | } 13 | addEventListener(callback) { 14 | this._messagingCallbacks.push(callback); 15 | } 16 | get conversationId() { 17 | return this.client.conversationId; 18 | } 19 | endConversation(userId) { 20 | this.client.postActivity({ 21 | type: "endOfConversation", 22 | from: { 23 | id: userId, 24 | } 25 | }) 26 | .subscribe( 27 | id => (id) => console.log("Conversation end request sent: " + id), 28 | error => { 29 | console.error("Error posting request to end conversation", error); 30 | } 31 | ); 32 | } 33 | removeEventListener(callback) { 34 | this._messagingCallbacks.splice(this._messagingCallbacks.indexOf(callback), 1); 35 | } 36 | sendMessage(user, message, userId) { 37 | // Patch if user isn't provided 38 | user = user || {}; 39 | const activity = { 40 | from: { 41 | id: userId, 42 | name: user.displayName, 43 | summary: user 44 | }, 45 | type: "message", 46 | text: message 47 | }; 48 | console.log("[BOT-FRAMEWORK] Sending activity: ", activity); 49 | this.client 50 | .postActivity(activity) 51 | .subscribe( 52 | id => (id) => console.log("Message sent with ID: " + id), 53 | error => { 54 | console.error("Error posting activity", error); 55 | } 56 | ); 57 | } 58 | } -------------------------------------------------------------------------------- /test/actions-on-google/MessageBuilderTest.js: -------------------------------------------------------------------------------- 1 | const MessageBuilder = require("../../lib/actions-on-google/MessageBuilder"); 2 | const assert = require('assert'); 3 | 4 | const instance = new MessageBuilder(); 5 | 6 | xdescribe('MessageBuilder', () => { 7 | 8 | it('should concat activities consisting of purely plain text', () => { 9 | const PLAIN_TEXT = [ 10 | { 11 | text: "This is a test..." 12 | }, 13 | { 14 | text: "Of some text." 15 | }, 16 | { 17 | text: "Being concatenated." 18 | } 19 | ]; 20 | let processed = instance.constructMessage(PLAIN_TEXT); 21 | assert.equal(processed.text, `${PLAIN_TEXT[0].text} . ${PLAIN_TEXT[1].text} . ${PLAIN_TEXT[2].text}`); 22 | }); 23 | 24 | it('should concat activities consisting of single audio cards', () => { 25 | const AUDIO_CARD_ACTIVITIES = [ 26 | { 27 | text: "This is a test...", 28 | attachments: [{ 29 | data: { 30 | contentType: "application/vnd.microsoft.card.audio", 31 | content: { 32 | media: [{ 33 | url: "http://test.audio-source.com/audio.mp3" 34 | }] 35 | } 36 | } 37 | }] 38 | }, 39 | { 40 | text: "Of some text." 41 | }, 42 | { 43 | text: "Being concatenated.", 44 | attachments: [{ 45 | data: { 46 | contentType: "application/vnd.microsoft.card.audio", 47 | content: { 48 | media: [{ 49 | url: "http://test.audio-source.com/audio-2.mp3" 50 | }] 51 | } 52 | } 53 | }] 54 | } 55 | ]; 56 | let processed = instance.constructMessage(AUDIO_CARD_ACTIVITIES); 57 | assert.equal(processed.text, `${AUDIO_CARD_ACTIVITIES[0].text} . ${AUDIO_CARD_ACTIVITIES[1].text} . ${AUDIO_CARD_ACTIVITIES[2].text} `); 58 | }); 59 | 60 | }); -------------------------------------------------------------------------------- /lib/actions-on-google/MessageBuilder.js: -------------------------------------------------------------------------------- 1 | const AttachmentTypes = require("../directline/model/AttachmentTypes.json"); 2 | 3 | module.exports = class MessageBuilder { 4 | 5 | constructor() { 6 | this._handleAttachments = this._handleAttachments.bind(this); 7 | } 8 | 9 | actOnMessages(activities, app, state) { 10 | let message = this._handleActivities(activities, app); 11 | console.log('Message constructed from activities', message); 12 | if (message.signInRequired) { 13 | console.log('Signin required'); 14 | app.askForSignIn(); 15 | } else { 16 | message.text = `${message.text}`; 17 | // Ask the user. 18 | app.ask(message.text, state); 19 | } 20 | } 21 | 22 | _handleActivities(activities) { 23 | console.log(`Handling activities ${JSON.stringify(activities)}`); 24 | let cardText = activity => activity.cards ? activity.cards.map(card => card.resolvedText).join(" ") : ""; 25 | 26 | return Object.assign({}, activities.reduce(this._flattenActivityCards), { 27 | text: activities 28 | .map(this._handleAttachments) 29 | .map(activity => activity.text + cardText(activity)) 30 | .join(" . ") 31 | }); 32 | } 33 | 34 | _flattenActivityCards(result, activity) { 35 | return Object.assign({}, result, { 36 | cards: [].concat(result.cards, activity.cards) 37 | }); 38 | } 39 | 40 | _handleAttachments(activity) { 41 | if (activity.attachments) { 42 | let cards = activity.attachments 43 | .map(attachment => { 44 | return attachment.data || attachment; 45 | }) 46 | .map((attachmentData) => { 47 | switch (attachmentData.contentType) { 48 | case AttachmentTypes.AudioCard: 49 | return this._renderAudioCard(activity, attachmentData); 50 | break; 51 | case AttachmentTypes.SigninCard: 52 | return this._renderSigninCard(attachmentData); 53 | break; 54 | default: 55 | return attachmentData; 56 | break; 57 | } 58 | }); 59 | return Object.assign({}, activity, { 60 | cards 61 | }); 62 | } else { 63 | return activity; 64 | } 65 | } 66 | 67 | _renderAudioCard(activity, attachment) { 68 | let text = activity.text; 69 | let content = attachment.content; 70 | return Object.assign({}, attachment, { 71 | resolvedText: ` ${content.media.map(item => ``).join(" ")} ` 72 | }); 73 | } 74 | 75 | _renderSigninCard(attachment) { 76 | return Object.assign({}, attachment, { 77 | signInRequired: true 78 | }); 79 | } 80 | } -------------------------------------------------------------------------------- /test/actions-on-google/MessageBuilder2Test.js: -------------------------------------------------------------------------------- 1 | const InstructionBuilder = require("../../lib/actions-on-google/InstructionBuilder"); 2 | const assert = require('assert'); 3 | 4 | const instance = new InstructionBuilder(); 5 | 6 | describe('MessageBuilder', () => { 7 | 8 | it('should concat activities consisting of purely plain text', () => { 9 | const PLAIN_TEXT = [ 10 | { 11 | text: "This is a test..." 12 | }, 13 | { 14 | text: "Of some text." 15 | }, 16 | { 17 | text: "Being concatenated." 18 | } 19 | ]; 20 | let processed = instance.constructInstructions(PLAIN_TEXT); 21 | assert.equal(processed.text, `${PLAIN_TEXT[0].text} . ${PLAIN_TEXT[1].text} . ${PLAIN_TEXT[2].text}`); 22 | }); 23 | 24 | it('should concat activities consisting of single audio cards', () => { 25 | const AUDIO_CARD_ACTIVITIES = [ 26 | { 27 | text: "This is a test...", 28 | attachments: [{ 29 | contentType: "application/vnd.microsoft.card.audio", 30 | content: { 31 | media: [{ 32 | url: "http://test.audio-source.com/audio.mp3" 33 | }] 34 | } 35 | }] 36 | }, 37 | { 38 | text: "Of some text." 39 | }, 40 | { 41 | text: "Being concatenated.", 42 | attachments: [{ 43 | contentType: "application/vnd.microsoft.card.audio", 44 | content: { 45 | media: [{ 46 | url: "http://test.audio-source.com/audio-2.mp3" 47 | }] 48 | } 49 | }] 50 | } 51 | ]; 52 | let processed = instance.constructInstructions(AUDIO_CARD_ACTIVITIES); 53 | assert.equal(processed.text, `${AUDIO_CARD_ACTIVITIES[0].text} . ${AUDIO_CARD_ACTIVITIES[1].text} . ${AUDIO_CARD_ACTIVITIES[2].text} `); 54 | }); 55 | 56 | it('should concat activities consisting of single audio cards', () => { 57 | const AUDIO_CARD_ACTIVITIES = [ 58 | { 59 | attachments: [{ 60 | contentType: "application/vnd.microsoft.card.signin", 61 | content: { 62 | text: "BotFramework Sign-in Card", 63 | buttons: [{ 64 | type: "signin", 65 | title: "Sign-in", 66 | value: "http://esurelogin-hosting-mobilehub-769067525.s3-website.eu-west-2.amazonaws.com/" 67 | }] 68 | } 69 | }], 70 | } 71 | ]; 72 | let processed = instance.constructInstructions(AUDIO_CARD_ACTIVITIES); 73 | assert.equal(processed.signInRequired, true); 74 | }); 75 | 76 | }); -------------------------------------------------------------------------------- /test/queue/GoogleOutboundQueueTest.js: -------------------------------------------------------------------------------- 1 | process.env.QUEUE_GROUPING_TIMEOUT = 250; 2 | const outboundMessageQueue = require("../../lib/queue/GoogleOutboundQueue"); 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | 6 | 7 | const mockMessage = { 8 | text: "This is my message", 9 | ask: sinon.spy() 10 | }; 11 | const mockMessage2 = { 12 | text: "This is my second message", 13 | ask: sinon.spy() 14 | }; 15 | 16 | describe('GoogleOutboundQueue', () => { 17 | it('should construct an empty map', () => { 18 | assert(outboundMessageQueue._queue instanceof Map); 19 | assert.equal(Array.from(outboundMessageQueue._queue.keys()).length, 0); 20 | // Clear interval for test purposes 21 | clearInterval(outboundMessageQueue._interval); 22 | }); 23 | it('should enqueue message correctly when no other messages queued for that conversation.', () => { 24 | // Pre assertions. 25 | assert(outboundMessageQueue._queue instanceof Map); 26 | assert.equal(Array.from(outboundMessageQueue._queue.keys()).length, 0); 27 | 28 | outboundMessageQueue.enqueueMessage("TEST_CONV_ID", mockMessage, mockMessage.ask); 29 | 30 | // Post assertions 31 | assert.equal(Array.from(outboundMessageQueue._queue.keys()).length, 1); 32 | assert.deepEqual(outboundMessageQueue._queue.get("TEST_CONV_ID"), { 33 | messages: [ 34 | mockMessage 35 | ] 36 | }); 37 | }); 38 | it('should enqueue subsequent messages correctly.', () => { 39 | 40 | // Pre assertions. 41 | assert(outboundMessageQueue._queue instanceof Map); 42 | assert.equal(Array.from(outboundMessageQueue._queue.keys()).length, 1); 43 | 44 | assert.deepEqual(outboundMessageQueue._queue.get("TEST_CONV_ID"), { 45 | messages: [ 46 | mockMessage 47 | ] 48 | }); 49 | 50 | outboundMessageQueue.enqueueMessage("TEST_CONV_ID", mockMessage2, mockMessage2.ask); 51 | 52 | // Post assertions 53 | assert.equal(Array.from(outboundMessageQueue._queue.keys()).length, 1); 54 | assert.deepEqual(outboundMessageQueue._queue.get("TEST_CONV_ID"), { 55 | messages: [ 56 | mockMessage, 57 | mockMessage2 58 | ] 59 | }); 60 | }); 61 | 62 | it('should call most recent ask() once with full message.', () => { 63 | // Pre assertions. 64 | assert.equal(Array.from(outboundMessageQueue._queue.keys()).length, 1); 65 | assert.deepEqual(outboundMessageQueue._queue.get("TEST_CONV_ID"), { 66 | messages: [ 67 | mockMessage, 68 | mockMessage2 69 | ] 70 | }); 71 | 72 | // Build count(); 73 | outboundMessageQueue.queueWorker(); 74 | assert(mockMessage.ask.notCalled); 75 | assert(mockMessage2.ask.notCalled); 76 | 77 | // Call Ask 78 | outboundMessageQueue.queueWorker(); 79 | 80 | // Post assertions 81 | assert(mockMessage.ask.notCalled); 82 | assert(mockMessage2.ask.calledOnce); 83 | }); 84 | }); -------------------------------------------------------------------------------- /lib/fulfilment.js: -------------------------------------------------------------------------------- 1 | const ActionsSdkApp = require('actions-on-google').ActionsSdkApp; 2 | const DirectLineManager = require("./directline/DirectLineManager"); 3 | const directLineClients = require("./directline/DirectLineClientStorage"); 4 | const outboundQueue = require("./queue/GoogleOutboundQueue"); 5 | const InstructionBuilder = require("./actions-on-google/InstructionBuilder"); 6 | 7 | const EXIT_TRIGGERS = ["bye", "exit", "quit"]; 8 | const EXIT_MESSAGE = "Ok, bye!"; 9 | let manager; 10 | const queuedMessages = new Map(); 11 | const assistantMessage = new InstructionBuilder(); 12 | 13 | const sendMessageToBot = (app, bot, input, userId, state, user) => { 14 | // Add event listener for responses 15 | let rand = Math.random(); 16 | let responder = event => { 17 | if (event.from.id !== app.getConversationId()) { 18 | console.log("[BOT-FRAMEWORK] Got response from bot:" + rand, event); 19 | outboundQueue.enqueueMessage(bot.conversationId, event, (messages) => { 20 | console.log("[ASKING] Sending messages: ", messages); 21 | const instructions = assistantMessage.constructInstructions(messages); 22 | state.lastInteraction = new Date(); 23 | assistantMessage.actOnInstructions(instructions, app, state); 24 | console.log("Acting on Instructions"); 25 | bot.removeEventListener(responder); 26 | }); 27 | } else if (!app.getConversationId()) { 28 | console.error("No conversation ID provided, cannot send message back."); 29 | } 30 | }; 31 | bot.addEventListener(responder); 32 | if (!user) { 33 | user = app.getUserName(); 34 | } 35 | bot.sendMessage(user, input, userId); 36 | }; 37 | 38 | const getDirectLineClient = (state, conversationTimeout, conversationId) => { 39 | // Check for expired conversation if timeout has been provided 40 | if (conversationTimeout && state.lastInteraction) { 41 | let bot = directLineClients.getClient(state.directlineConversationId); 42 | // Get time delta from state's last interaction 43 | const delta = new Date() - new Date(state.lastInteraction); 44 | // If the delta is large enough to be >= the provided timeout 45 | if (delta >= conversationTimeout) { 46 | // End the conversation 47 | bot.endConversation(conversationId); 48 | // Make bot null 49 | bot = null; 50 | // Remove the directline client from the store 51 | directLineClients.removeClient(state.directlineConversationId); 52 | // Remove conversationId from the state 53 | delete state.directlineConversationId; 54 | delete state.lastInteraction; 55 | } 56 | } 57 | // Get an instance of the directline client 58 | let bot = directLineClients.getClient(state.directlineConversationId); 59 | console.log("STATE:", state); 60 | if (!bot) { 61 | // Create a conversation 62 | return manager.createConversation(); 63 | } else { 64 | return Promise.resolve(bot); 65 | } 66 | } 67 | 68 | let continueConversation = function (input, app, state, conversationTimeout, shouldGetUsersNameFromGoogle, user) { 69 | const conversationId = app.getConversationId(); 70 | // If user is trying to exit. 71 | if (EXIT_TRIGGERS.includes(input.toLowerCase())) { 72 | console.log("[EXIT] User requested exit"); 73 | app.tell(EXIT_MESSAGE); 74 | } else { 75 | getDirectLineClient(state, conversationTimeout, conversationId) 76 | .then(bot => { 77 | // Update state to contain conversation ID 78 | state.directlineConversationId = bot.conversationId; 79 | // Store in client storage 80 | directLineClients.storeClient(bot.conversationId, bot); 81 | // Get user name 82 | let userName = app.getUserName(); 83 | // If we can't get the user's name.... 84 | if (shouldGetUsersNameFromGoogle && (!user || !app.getUserName())) { 85 | const permission = app.SupportedPermissions.NAME; 86 | // Request permissions 87 | app.askForPermission('To know who you are', permission, state); 88 | // Add a message to the queue, to be sent on permission granted. 89 | queuedMessages.set(state.directlineConversationId, (newApp) => sendMessageToBot(newApp, bot, input, conversationId, state)); 90 | } else { 91 | // Else - if we can - Send message to directline 92 | sendMessageToBot(app, bot, input, conversationId, state, user || app.getUserName()); 93 | } 94 | }); 95 | } 96 | }; 97 | 98 | const fulfill = (onUserSignedInHandlerProvider, conversationTimeout, shouldGetUsersNameFromGoogle) => (app) => { 99 | let state = Object.assign({}, app.getDialogState()); 100 | let intent = app.getIntent(); 101 | 102 | let input = app.getRawInput(); 103 | 104 | if (intent === app.StandardIntents.PERMISSION) { 105 | console.log("Executing queued message"); 106 | queuedMessages.get(state.directlineConversationId)(app); 107 | return queuedMessages.delete(state.directlineConversationId); 108 | } else if (intent === app.StandardIntents.SIGN_IN) { 109 | const user = app.getUser(); 110 | if (app.getSignInStatus() === app.SignInStatus.OK) { 111 | console.log('User has signed in: ', user); 112 | //Get id token and populate user with it 113 | const onUserSignedInHandler = onUserSignedInHandlerProvider.getHandler(); 114 | onUserSignedInHandler(user).then((res) => { 115 | input = 'Hi'; 116 | continueConversation(input, app, state, conversationTimeout, shouldGetUsersNameFromGoogle, res); 117 | }); 118 | } else { 119 | app.tell('You need to sign-in before using the app. Use Google assistant on the Google Home app on your phone to sign in.'); 120 | } 121 | } else { 122 | continueConversation(input, app, state, conversationTimeout, shouldGetUsersNameFromGoogle); 123 | } 124 | }; 125 | 126 | module.exports = (directlineSecret, onUserSignedInHandlerProvider, conversationTimeout, shouldGetUsersNameFromGoogle) => { 127 | manager = new DirectLineManager(directlineSecret); 128 | return (req, res) => { 129 | console.log("Inbound request to: " + req.url) 130 | const app = new ActionsSdkApp({ 131 | request: req, 132 | response: res 133 | }); 134 | 135 | app.handleRequest(fulfill(onUserSignedInHandlerProvider, conversationTimeout, shouldGetUsersNameFromGoogle)); 136 | } 137 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bot-framework-actions-on-google 2 | 3 | 4 | This NPM Module adds a Microsoft Bot Framework integration to Actions on Google - turning it into an input channel. 5 | 6 | [![CircleCI](https://circleci.com/gh/Capgemini-AIE/bot-framework-actions-on-google.svg?style=svg&circle-token=9cc914f06f298c6d0bed0886b943f177b89ad883)](https://circleci.com/gh/Capgemini-AIE/bot-framework-actions-on-google) 7 | 8 | ## Quickstart 9 | 10 | If you know how to create an Actions on Google project with the Actions SDK. Follow the usual process of creating and populating an actions.json file. 11 | 12 | This module then acts as a replacement for your fulfilment, simply use the module to construct an express router to be used as the fulfilment URL. You will need to provide a directline secret. 13 | 14 | ```javascript 15 | const actionsOnGoogleAdapter = require("bot-framework-actions-on-google"); 16 | const express = require("express"); 17 | 18 | const app = express(); 19 | 20 | // Construct and use router. 21 | app.use(actionsOnGoogleAdapter()); 22 | ``` 23 | 24 | ## Full Guide 25 | 26 | ### Setting up actions.json 27 | ```json 28 | { 29 | "actions": [{ 30 | "description": "", 31 | "name": "MAIN", 32 | "fulfillment": { 33 | "conversationName": "MAIN_CONVERSATION" 34 | }, 35 | "intent": { 36 | "name": "actions.intent.MAIN", 37 | "trigger": { 38 | "queryPatterns": ["talk to "] 39 | } 40 | } 41 | }], 42 | "conversations": { 43 | "MAIN_CONVERSATION": { 44 | "name": "MAIN_CONVERSATION", 45 | "url": "" 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ### Providing Google with your actions.json file 52 | 53 | Now that you've got an actions.json file - you need to update Google on the configuration of your action. 54 | 55 | `gactions update --action_package actions.json --project ` 56 | 57 | ### Using the module 58 | 59 | #### Router 60 | 61 | This module exposes a single function, which returns an object which has an Express Router and a way to register a handler for when a user links their account. 62 | 63 | `actionsOnGoogleAdapter();` 64 | 65 | Simply require() the module, and pass it your Microsoft Bot Framework Directline secret. 66 | 67 | Then pass the resulting router to ExpressJS's `app.use()` middleware registration function. 68 | 69 | ```javascript 70 | //========================================================= 71 | // Import NPM modules 72 | //========================================================= 73 | const express = require("express"); 74 | const actionsOnGoogleAdapter = require("bot-framework-actions-on-google"); 75 | 76 | const app = express(); 77 | 78 | app.use(actionsOnGoogleAdapter().router); 79 | 80 | const PORT = process.env.PORT || 3000; 81 | app.listen(PORT, () => console.log(`ActionsOnGoogle demo listening on port ${PORT}!`)); 82 | ``` 83 | 84 | Now that this is complete. Deploy the express server to the URL you configured in the actions.json file - and your bot should be accessible through Actions on Google/Google Assistant. 85 | 86 | #### Sign In Handler 87 | 88 | Use the sign in handler to get any additional information about your user or anything else once you have an access token. 89 | 90 | The access token is available on the user object passed into the handler. 91 | 92 | ```javascript 93 | actionsOnGoogleAdapter.onUserSignedInHandlerProvider.registerHandler((user) => { 94 | // Get additional user details from your API once account is linked and access token is available 95 | return fetch('https://your-api/user/' + user.userId, { 96 | headers: { 97 | Authorization: `Bearer ${user.accessToken}` 98 | } 99 | }).then(res => res.json()) 100 | .then(res => ({...res, ...user})) 101 | }); 102 | ``` 103 | 104 | ## Features 105 | 106 | ### Controlling the session (should we wait for another message from the user?) 107 | 108 | By default this bridge will leave the session open after returning a response to Google and will expect further input from the user, which will allow them to continue their conversation with your action. If you wish to end the session by default following each message, there an environment setting 'DEFAULT_LEAVE_SESSION_OPEN' which you can set to "false" to achieve this. 109 | 110 | This bridge also supports InputHint properties on Bot Framework activities when using the SayAsync method (as opposed to PostAsync), which determine if the bot should wait for further input from the user in a voice scenario. 111 | 112 | The bridge looks for an inputHint on the incoming activity from the bot, specifically looking for either the 'ExpectingInput' hint, which will cause the bridge to leave the conversation open, or 'IgnoringInput', which will end the conversation. So, for example, if you have sessions being left open by default you could use the 'IgnoringInput' InputHint to end the session. Conversely, if you end the session by default, you can send the 'ExpectingInput' InputHint on your bot activity to indicate that we should wait for the user to say something else. 113 | 114 | Below is an example of using the above InputHint features in a C# bot. In this example we send a message from the bot to the bridge and also indicate that we are expecting a further message from the user. 115 | 116 | ```cs 117 | var messageText = "Thanks! Can I help you with something else?"; 118 | var messageOptions = new MessageOptions 119 | { 120 | InputHint = InputHints.ExpectingInput 121 | }; 122 | await context.SayAsync(messageText, speakText, options: messageOptions); 123 | ``` 124 | 125 | ### Audio Cards 126 | 127 | This bridge supports Microsoft Bot Framework Audio Cards: 128 | (https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.audiocard.html) 129 | 130 | It will simply concatenate the audio file onto the text provided in the response. 131 | 132 | 133 | ### Sign In Cards 134 | 135 | This bridge supports Microsoft Bot Framework Signin Cards: 136 | (https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.signincard.html) 137 | 138 | If a sign in card is sent back, other cards are ignored and the user will be asked to sign in. 139 | 140 | ## Known Issues 141 | 142 | ### Multiple Responses 143 | 144 | Actions on Google doesn't play too nicely with multiple messages being sent, ideally keep your responses in one message block. 145 | -------------------------------------------------------------------------------- /test/directline/DirectLineWrapperTest.js: -------------------------------------------------------------------------------- 1 | const DirectLineWrapper = require("../../lib/directline/DirectLineWrapper"); 2 | const assert = require('assert'); 3 | const sinon = require('sinon'); 4 | 5 | const mockClient = { 6 | conversationId: "AB123-00", 7 | isDirectLineClient: true, 8 | activity$: { 9 | filter: () => ({ 10 | subscribe: () => { 11 | 12 | } 13 | }) 14 | } 15 | }; 16 | 17 | const mockUser = { 18 | id: 123912456, 19 | isChatbotUser: true 20 | }; 21 | 22 | describe('DirectLineWrapper', () => { 23 | it('should initialise correctly, storing the client provided on construction', () => { 24 | let wrapper = new DirectLineWrapper(mockClient, mockUser); 25 | assert.equal(wrapper.client, mockClient); 26 | assert.equal(wrapper._user, mockUser); 27 | assert.deepEqual(wrapper._messagingCallbacks, []); 28 | }); 29 | 30 | it('should correctly subscribe to an event', () => { 31 | let subscribeCompatibleMock = Object.assign({}, mockClient, { 32 | activity$: { 33 | filter: (filterFunction) => { 34 | assert.equal(filterFunction({ 35 | type: "message" 36 | }), true) 37 | assert.equal(filterFunction({ 38 | type: "conversation" 39 | }), false) 40 | return ({ 41 | subscribe: (callback) => { 42 | assert.equal(callback instanceof Function, true); 43 | } 44 | }) 45 | } 46 | } 47 | }) 48 | let wrapper = new DirectLineWrapper(subscribeCompatibleMock, mockUser); 49 | assert.equal(mockClient.conversationId, wrapper.conversationId); 50 | }); 51 | 52 | it('should call registered event listener on activity', () => { 53 | // Setup mocks 54 | let subscriptionFunction; 55 | let subscribeCompatibleMock = Object.assign({}, mockClient, { 56 | activity$: { 57 | filter: (filterFunction) => { 58 | assert.equal(filterFunction({ 59 | type: "message" 60 | }), true) 61 | assert.equal(filterFunction({ 62 | type: "conversation" 63 | }), false) 64 | return ({ 65 | subscribe: (callback) => { 66 | assert.equal(callback instanceof Function, true); 67 | subscriptionFunction = callback; 68 | } 69 | }) 70 | } 71 | } 72 | }); 73 | const mockActivity = { 74 | id: 12341, 75 | isActivity: true 76 | }; 77 | const eventHandler = sinon.spy(); 78 | // Construct wrapper 79 | let wrapper = new DirectLineWrapper(subscribeCompatibleMock, mockUser); 80 | // Call event registration 81 | wrapper.addEventListener(eventHandler); 82 | // Trigger activity 83 | subscriptionFunction(mockActivity); 84 | // Assert 85 | assert(eventHandler.calledOnce); 86 | assert(eventHandler.calledWith(mockActivity)); 87 | }); 88 | 89 | it('should call multiple registered event listeners on activity', () => { 90 | // Setup mocks 91 | let subscriptionFunction; 92 | let subscribeCompatibleMock = Object.assign({}, mockClient, { 93 | activity$: { 94 | filter: (filterFunction) => { 95 | assert.equal(filterFunction({ 96 | type: "message" 97 | }), true) 98 | assert.equal(filterFunction({ 99 | type: "conversation" 100 | }), false) 101 | return ({ 102 | subscribe: (callback) => { 103 | assert.equal(callback instanceof Function, true); 104 | subscriptionFunction = callback; 105 | } 106 | }) 107 | } 108 | } 109 | }); 110 | const mockActivity = { 111 | id: 12341, 112 | isActivity: true 113 | }; 114 | const eventHandlerOne = sinon.spy(); 115 | // Construct wrapper 116 | let wrapper = new DirectLineWrapper(subscribeCompatibleMock, mockUser); 117 | // Call event registration 118 | wrapper.addEventListener(eventHandlerOne); 119 | // Trigger activity 120 | subscriptionFunction(mockActivity); 121 | // Assert 122 | assert(eventHandlerOne.calledOnce); 123 | assert(eventHandlerOne.calledWith(mockActivity)); 124 | 125 | /** 126 | * Second call 127 | */ 128 | const eventHandlerTwo = sinon.spy(); 129 | // Call event registration 130 | wrapper.addEventListener(eventHandlerTwo); 131 | // Trigger activity 132 | subscriptionFunction(mockActivity); 133 | // Handler 1 134 | assert(eventHandlerOne.callCount, 2); 135 | assert(eventHandlerOne.called); 136 | assert(eventHandlerOne.calledWith(mockActivity)); 137 | // Handler 2 138 | assert(eventHandlerTwo.calledOnce); 139 | assert(eventHandlerTwo.calledWith(mockActivity)); 140 | 141 | }); 142 | 143 | it('should return the correct conversation ID when getter is called', () => { 144 | let wrapper = new DirectLineWrapper(mockClient, mockUser); 145 | assert.equal(mockClient.conversationId, wrapper.conversationId); 146 | }); 147 | 148 | it('should be able to remove registered event listener', () => { 149 | // Setup mocks 150 | let subscriptionFunction; 151 | let subscribeCompatibleMock = Object.assign({}, mockClient, { 152 | activity$: { 153 | filter: (filterFunction) => { 154 | assert.equal(filterFunction({ 155 | type: "message" 156 | }), true) 157 | assert.equal(filterFunction({ 158 | type: "conversation" 159 | }), false) 160 | return ({ 161 | subscribe: (callback) => { 162 | assert.equal(callback instanceof Function, true); 163 | subscriptionFunction = callback; 164 | } 165 | }) 166 | } 167 | } 168 | }); 169 | const mockActivity = { 170 | id: 12341, 171 | isActivity: true 172 | }; 173 | const eventHandler = sinon.spy(); 174 | // Construct wrapper 175 | let wrapper = new DirectLineWrapper(subscribeCompatibleMock, mockUser); 176 | // Call event registration 177 | wrapper.addEventListener(eventHandler); 178 | // Trigger activity 179 | subscriptionFunction(mockActivity); 180 | // Remove event listener 181 | wrapper.removeEventListener(eventHandler); 182 | // Trigger activity again 183 | subscriptionFunction(mockActivity); 184 | // Assert 185 | assert(eventHandler.calledOnce); 186 | assert(eventHandler.calledWith(mockActivity)); 187 | }); 188 | 189 | 190 | it('should send a well constructed event when sendMessage is called', () => { 191 | // Mocks 192 | const user = { 193 | displayName: "Dan Cotton" 194 | }; 195 | const userId = "AA-B123-00A"; 196 | const message = "This is a test message"; 197 | const activityMock = { 198 | from: { 199 | id: userId, 200 | name: user.displayName, 201 | summary: user 202 | }, 203 | type: "message", 204 | text: message 205 | }; 206 | const postActivityMockClient = Object.assign({}, mockClient, { 207 | postActivity: () => ({ 208 | subscribe: () => {} 209 | }) 210 | }); 211 | let spy = sinon.spy(postActivityMockClient, "postActivity"); 212 | // Construct Wrapper, perform send message 213 | let wrapper = new DirectLineWrapper(postActivityMockClient, mockUser); 214 | wrapper.sendMessage(user, message, userId); 215 | assert(spy.calledOnce); 216 | assert(spy.calledWith(activityMock)); 217 | }); 218 | 219 | }); 220 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/node@^8.0.19": 6 | version "8.5.5" 7 | resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.5.tgz#6f9e8164ae1a55a9beb1d2571cfb7acf9d720c61" 8 | 9 | abbrev@1: 10 | version "1.1.1" 11 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 12 | 13 | accepts@~1.3.4: 14 | version "1.3.4" 15 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" 16 | dependencies: 17 | mime-types "~2.1.16" 18 | negotiator "0.6.1" 19 | 20 | actions-on-google@^1.7.0: 21 | version "1.7.0" 22 | resolved "https://registry.yarnpkg.com/actions-on-google/-/actions-on-google-1.7.0.tgz#4bc7209bd696755affb56c61fa4e7da7896075b9" 23 | dependencies: 24 | debug "^2.2.0" 25 | google-auth-library "^0.10.0" 26 | lodash.camelcase "^4.3.0" 27 | lodash.snakecase "^4.1.1" 28 | 29 | ajv@^5.1.0: 30 | version "5.5.2" 31 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" 32 | dependencies: 33 | co "^4.6.0" 34 | fast-deep-equal "^1.0.0" 35 | fast-json-stable-stringify "^2.0.0" 36 | json-schema-traverse "^0.3.0" 37 | 38 | ansi@^0.3.0, ansi@~0.3.1: 39 | version "0.3.1" 40 | resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" 41 | 42 | are-we-there-yet@~1.1.2: 43 | version "1.1.4" 44 | resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" 45 | dependencies: 46 | delegates "^1.0.0" 47 | readable-stream "^2.0.6" 48 | 49 | array-flatten@1.1.1: 50 | version "1.1.1" 51 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 52 | 53 | asn1@~0.2.3: 54 | version "0.2.3" 55 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" 56 | 57 | assert-plus@1.0.0, assert-plus@^1.0.0: 58 | version "1.0.0" 59 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 60 | 61 | async@^2.3.0: 62 | version "2.6.0" 63 | resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" 64 | dependencies: 65 | lodash "^4.14.0" 66 | 67 | asynckit@^0.4.0: 68 | version "0.4.0" 69 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 70 | 71 | aws-sign2@~0.7.0: 72 | version "0.7.0" 73 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 74 | 75 | aws4@^1.6.0: 76 | version "1.6.0" 77 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" 78 | 79 | balanced-match@^1.0.0: 80 | version "1.0.0" 81 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 82 | 83 | base64url@2.0.0, base64url@^2.0.0: 84 | version "2.0.0" 85 | resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" 86 | 87 | bcrypt-pbkdf@^1.0.0: 88 | version "1.0.1" 89 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" 90 | dependencies: 91 | tweetnacl "^0.14.3" 92 | 93 | binary@^0.3.0: 94 | version "0.3.0" 95 | resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" 96 | dependencies: 97 | buffers "~0.1.1" 98 | chainsaw "~0.1.0" 99 | 100 | body-parser@1.18.2, body-parser@^1.18.2: 101 | version "1.18.2" 102 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" 103 | dependencies: 104 | bytes "3.0.0" 105 | content-type "~1.0.4" 106 | debug "2.6.9" 107 | depd "~1.1.1" 108 | http-errors "~1.6.2" 109 | iconv-lite "0.4.19" 110 | on-finished "~2.3.0" 111 | qs "6.5.1" 112 | raw-body "2.3.2" 113 | type-is "~1.6.15" 114 | 115 | boom@4.x.x: 116 | version "4.3.1" 117 | resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" 118 | dependencies: 119 | hoek "4.x.x" 120 | 121 | boom@5.x.x: 122 | version "5.2.0" 123 | resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" 124 | dependencies: 125 | hoek "4.x.x" 126 | 127 | botframework-directlinejs@^0.9.14: 128 | version "0.9.14" 129 | resolved "https://registry.yarnpkg.com/botframework-directlinejs/-/botframework-directlinejs-0.9.14.tgz#cd7b0c613b9eb7c1963a0cc2a4df11b7a54360d7" 130 | dependencies: 131 | rxjs "^5.0.3" 132 | 133 | brace-expansion@^1.1.7: 134 | version "1.1.8" 135 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" 136 | dependencies: 137 | balanced-match "^1.0.0" 138 | concat-map "0.0.1" 139 | 140 | browser-stdout@1.3.0: 141 | version "1.3.0" 142 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" 143 | 144 | buffer-equal-constant-time@1.0.1: 145 | version "1.0.1" 146 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 147 | 148 | buffers@~0.1.1: 149 | version "0.1.1" 150 | resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" 151 | 152 | builtin-modules@^1.0.0: 153 | version "1.1.1" 154 | resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" 155 | 156 | bytes@3.0.0: 157 | version "3.0.0" 158 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 159 | 160 | caseless@~0.12.0: 161 | version "0.12.0" 162 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 163 | 164 | chainsaw@~0.1.0: 165 | version "0.1.0" 166 | resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" 167 | dependencies: 168 | traverse ">=0.3.0 <0.4" 169 | 170 | charenc@~0.0.1: 171 | version "0.0.2" 172 | resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" 173 | 174 | chownr@^1.0.1: 175 | version "1.0.1" 176 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" 177 | 178 | co@^4.6.0: 179 | version "4.6.0" 180 | resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 181 | 182 | combined-stream@^1.0.5, combined-stream@~1.0.5: 183 | version "1.0.5" 184 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" 185 | dependencies: 186 | delayed-stream "~1.0.0" 187 | 188 | commander@2.11.0: 189 | version "2.11.0" 190 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" 191 | 192 | commander@2.9.0: 193 | version "2.9.0" 194 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" 195 | dependencies: 196 | graceful-readlink ">= 1.0.0" 197 | 198 | concat-map@0.0.1: 199 | version "0.0.1" 200 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 201 | 202 | concat-stream@^1.4.6: 203 | version "1.6.0" 204 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" 205 | dependencies: 206 | inherits "^2.0.3" 207 | readable-stream "^2.2.2" 208 | typedarray "^0.0.6" 209 | 210 | content-disposition@0.5.2: 211 | version "0.5.2" 212 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 213 | 214 | content-type@~1.0.4: 215 | version "1.0.4" 216 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 217 | 218 | cookie-signature@1.0.6: 219 | version "1.0.6" 220 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 221 | 222 | cookie@0.3.1: 223 | version "0.3.1" 224 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 225 | 226 | core-util-is@1.0.2, core-util-is@~1.0.0: 227 | version "1.0.2" 228 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 229 | 230 | crypt@~0.0.1: 231 | version "0.0.2" 232 | resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" 233 | 234 | cryptiles@3.x.x: 235 | version "3.1.2" 236 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" 237 | dependencies: 238 | boom "5.x.x" 239 | 240 | dashdash@^1.12.0: 241 | version "1.14.1" 242 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 243 | dependencies: 244 | assert-plus "^1.0.0" 245 | 246 | debug@2.6.8: 247 | version "2.6.8" 248 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" 249 | dependencies: 250 | ms "2.0.0" 251 | 252 | debug@2.6.9, debug@^2.2.0: 253 | version "2.6.9" 254 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 255 | dependencies: 256 | ms "2.0.0" 257 | 258 | debug@3.1.0: 259 | version "3.1.0" 260 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 261 | dependencies: 262 | ms "2.0.0" 263 | 264 | decompress-zip@^0.3.0: 265 | version "0.3.0" 266 | resolved "https://registry.yarnpkg.com/decompress-zip/-/decompress-zip-0.3.0.tgz#ae3bcb7e34c65879adfe77e19c30f86602b4bdb0" 267 | dependencies: 268 | binary "^0.3.0" 269 | graceful-fs "^4.1.3" 270 | mkpath "^0.1.0" 271 | nopt "^3.0.1" 272 | q "^1.1.2" 273 | readable-stream "^1.1.8" 274 | touch "0.0.3" 275 | 276 | delayed-stream@~1.0.0: 277 | version "1.0.0" 278 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 279 | 280 | delegates@^1.0.0: 281 | version "1.0.0" 282 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 283 | 284 | depd@1.1.1, depd@~1.1.1: 285 | version "1.1.1" 286 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 287 | 288 | destroy@~1.0.4: 289 | version "1.0.4" 290 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 291 | 292 | diff@3.2.0: 293 | version "3.2.0" 294 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" 295 | 296 | diff@3.3.1: 297 | version "3.3.1" 298 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" 299 | 300 | diff@^3.1.0: 301 | version "3.4.0" 302 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" 303 | 304 | dotenv@^4.0.0: 305 | version "4.0.0" 306 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" 307 | 308 | ecc-jsbn@~0.1.1: 309 | version "0.1.1" 310 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" 311 | dependencies: 312 | jsbn "~0.1.0" 313 | 314 | ecdsa-sig-formatter@1.0.9: 315 | version "1.0.9" 316 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" 317 | dependencies: 318 | base64url "^2.0.0" 319 | safe-buffer "^5.0.1" 320 | 321 | ee-first@1.1.1: 322 | version "1.1.1" 323 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 324 | 325 | encodeurl@~1.0.1: 326 | version "1.0.1" 327 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 328 | 329 | encoding@^0.1.11: 330 | version "0.1.12" 331 | resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" 332 | dependencies: 333 | iconv-lite "~0.4.13" 334 | 335 | escape-html@~1.0.3: 336 | version "1.0.3" 337 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 338 | 339 | escape-string-regexp@1.0.5: 340 | version "1.0.5" 341 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 342 | 343 | etag@~1.8.1: 344 | version "1.8.1" 345 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 346 | 347 | express@^4.16.2: 348 | version "4.16.2" 349 | resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" 350 | dependencies: 351 | accepts "~1.3.4" 352 | array-flatten "1.1.1" 353 | body-parser "1.18.2" 354 | content-disposition "0.5.2" 355 | content-type "~1.0.4" 356 | cookie "0.3.1" 357 | cookie-signature "1.0.6" 358 | debug "2.6.9" 359 | depd "~1.1.1" 360 | encodeurl "~1.0.1" 361 | escape-html "~1.0.3" 362 | etag "~1.8.1" 363 | finalhandler "1.1.0" 364 | fresh "0.5.2" 365 | merge-descriptors "1.0.1" 366 | methods "~1.1.2" 367 | on-finished "~2.3.0" 368 | parseurl "~1.3.2" 369 | path-to-regexp "0.1.7" 370 | proxy-addr "~2.0.2" 371 | qs "6.5.1" 372 | range-parser "~1.2.0" 373 | safe-buffer "5.1.1" 374 | send "0.16.1" 375 | serve-static "1.13.1" 376 | setprototypeof "1.1.0" 377 | statuses "~1.3.1" 378 | type-is "~1.6.15" 379 | utils-merge "1.0.1" 380 | vary "~1.1.2" 381 | 382 | extend@~3.0.1: 383 | version "3.0.1" 384 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" 385 | 386 | extsprintf@1.3.0: 387 | version "1.3.0" 388 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 389 | 390 | extsprintf@^1.2.0: 391 | version "1.4.0" 392 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 393 | 394 | fast-deep-equal@^1.0.0: 395 | version "1.0.0" 396 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" 397 | 398 | fast-json-stable-stringify@^2.0.0: 399 | version "2.0.0" 400 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" 401 | 402 | finalhandler@1.1.0: 403 | version "1.1.0" 404 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" 405 | dependencies: 406 | debug "2.6.9" 407 | encodeurl "~1.0.1" 408 | escape-html "~1.0.3" 409 | on-finished "~2.3.0" 410 | parseurl "~1.3.2" 411 | statuses "~1.3.1" 412 | unpipe "~1.0.0" 413 | 414 | forever-agent@~0.6.1: 415 | version "0.6.1" 416 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 417 | 418 | form-data@~2.3.1: 419 | version "2.3.1" 420 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" 421 | dependencies: 422 | asynckit "^0.4.0" 423 | combined-stream "^1.0.5" 424 | mime-types "^2.1.12" 425 | 426 | formatio@1.2.0, formatio@^1.2.0: 427 | version "1.2.0" 428 | resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" 429 | dependencies: 430 | samsam "1.x" 431 | 432 | forwarded@~0.1.2: 433 | version "0.1.2" 434 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 435 | 436 | fresh@0.5.2: 437 | version "0.5.2" 438 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 439 | 440 | fs.realpath@^1.0.0: 441 | version "1.0.0" 442 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 443 | 444 | gauge@~1.2.5: 445 | version "1.2.7" 446 | resolved "https://registry.yarnpkg.com/gauge/-/gauge-1.2.7.tgz#e9cec5483d3d4ee0ef44b60a7d99e4935e136d93" 447 | dependencies: 448 | ansi "^0.3.0" 449 | has-unicode "^2.0.0" 450 | lodash.pad "^4.1.0" 451 | lodash.padend "^4.1.0" 452 | lodash.padstart "^4.1.0" 453 | 454 | getpass@^0.1.1: 455 | version "0.1.7" 456 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 457 | dependencies: 458 | assert-plus "^1.0.0" 459 | 460 | glob@7.1.1: 461 | version "7.1.1" 462 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" 463 | dependencies: 464 | fs.realpath "^1.0.0" 465 | inflight "^1.0.4" 466 | inherits "2" 467 | minimatch "^3.0.2" 468 | once "^1.3.0" 469 | path-is-absolute "^1.0.0" 470 | 471 | glob@7.1.2, glob@^7.0.5: 472 | version "7.1.2" 473 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 474 | dependencies: 475 | fs.realpath "^1.0.0" 476 | inflight "^1.0.4" 477 | inherits "2" 478 | minimatch "^3.0.4" 479 | once "^1.3.0" 480 | path-is-absolute "^1.0.0" 481 | 482 | google-auth-library@^0.10.0: 483 | version "0.10.0" 484 | resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e" 485 | dependencies: 486 | gtoken "^1.2.1" 487 | jws "^3.1.4" 488 | lodash.noop "^3.0.1" 489 | request "^2.74.0" 490 | 491 | google-p12-pem@^0.1.0: 492 | version "0.1.2" 493 | resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177" 494 | dependencies: 495 | node-forge "^0.7.1" 496 | 497 | graceful-fs@^4.1.2, graceful-fs@^4.1.3: 498 | version "4.1.11" 499 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 500 | 501 | "graceful-readlink@>= 1.0.0": 502 | version "1.0.1" 503 | resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" 504 | 505 | growl@1.10.3: 506 | version "1.10.3" 507 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" 508 | 509 | growl@1.9.2: 510 | version "1.9.2" 511 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" 512 | 513 | gtoken@^1.2.1: 514 | version "1.2.3" 515 | resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8" 516 | dependencies: 517 | google-p12-pem "^0.1.0" 518 | jws "^3.0.0" 519 | mime "^1.4.1" 520 | request "^2.72.0" 521 | 522 | har-schema@^2.0.0: 523 | version "2.0.0" 524 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 525 | 526 | har-validator@~5.0.3: 527 | version "5.0.3" 528 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" 529 | dependencies: 530 | ajv "^5.1.0" 531 | har-schema "^2.0.0" 532 | 533 | has-flag@^1.0.0: 534 | version "1.0.0" 535 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" 536 | 537 | has-flag@^2.0.0: 538 | version "2.0.0" 539 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" 540 | 541 | has-unicode@^2.0.0: 542 | version "2.0.1" 543 | resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 544 | 545 | hawk@~6.0.2: 546 | version "6.0.2" 547 | resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" 548 | dependencies: 549 | boom "4.x.x" 550 | cryptiles "3.x.x" 551 | hoek "4.x.x" 552 | sntp "2.x.x" 553 | 554 | he@1.1.1: 555 | version "1.1.1" 556 | resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" 557 | 558 | hoek@4.x.x: 559 | version "4.2.0" 560 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" 561 | 562 | hosted-git-info@^2.1.4, hosted-git-info@^2.1.5: 563 | version "2.5.0" 564 | resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" 565 | 566 | http-errors@1.6.2, http-errors@~1.6.2: 567 | version "1.6.2" 568 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 569 | dependencies: 570 | depd "1.1.1" 571 | inherits "2.0.3" 572 | setprototypeof "1.0.3" 573 | statuses ">= 1.3.1 < 2" 574 | 575 | http-signature@~1.2.0: 576 | version "1.2.0" 577 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 578 | dependencies: 579 | assert-plus "^1.0.0" 580 | jsprim "^1.2.2" 581 | sshpk "^1.7.0" 582 | 583 | iconv-lite@0.4.19, iconv-lite@~0.4.13: 584 | version "0.4.19" 585 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 586 | 587 | inflight@^1.0.4: 588 | version "1.0.6" 589 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 590 | dependencies: 591 | once "^1.3.0" 592 | wrappy "1" 593 | 594 | inherits@2, inherits@2.0.3, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: 595 | version "2.0.3" 596 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 597 | 598 | ipaddr.js@1.5.2: 599 | version "1.5.2" 600 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" 601 | 602 | is-buffer@~1.1.1: 603 | version "1.1.6" 604 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 605 | 606 | is-builtin-module@^1.0.0: 607 | version "1.0.0" 608 | resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" 609 | dependencies: 610 | builtin-modules "^1.0.0" 611 | 612 | is-stream@^1.0.1: 613 | version "1.1.0" 614 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 615 | 616 | is-typedarray@~1.0.0: 617 | version "1.0.0" 618 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 619 | 620 | isarray@0.0.1: 621 | version "0.0.1" 622 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 623 | 624 | isarray@~1.0.0: 625 | version "1.0.0" 626 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 627 | 628 | isstream@~0.1.2: 629 | version "0.1.2" 630 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 631 | 632 | jsbn@~0.1.0: 633 | version "0.1.1" 634 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 635 | 636 | json-schema-traverse@^0.3.0: 637 | version "0.3.1" 638 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" 639 | 640 | json-schema@0.2.3: 641 | version "0.2.3" 642 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 643 | 644 | json-stringify-safe@~5.0.1: 645 | version "5.0.1" 646 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 647 | 648 | json3@3.3.2: 649 | version "3.3.2" 650 | resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" 651 | 652 | jsprim@^1.2.2: 653 | version "1.4.1" 654 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 655 | dependencies: 656 | assert-plus "1.0.0" 657 | extsprintf "1.3.0" 658 | json-schema "0.2.3" 659 | verror "1.10.0" 660 | 661 | just-extend@^1.1.26: 662 | version "1.1.27" 663 | resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" 664 | 665 | jwa@^1.1.4: 666 | version "1.1.5" 667 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" 668 | dependencies: 669 | base64url "2.0.0" 670 | buffer-equal-constant-time "1.0.1" 671 | ecdsa-sig-formatter "1.0.9" 672 | safe-buffer "^5.0.1" 673 | 674 | jws@^3.0.0, jws@^3.1.4: 675 | version "3.1.4" 676 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" 677 | dependencies: 678 | base64url "^2.0.0" 679 | jwa "^1.1.4" 680 | safe-buffer "^5.0.1" 681 | 682 | lock@^0.1.2: 683 | version "0.1.4" 684 | resolved "https://registry.yarnpkg.com/lock/-/lock-0.1.4.tgz#fec7deaef17e7c3a0a55e1da042803e25d91745d" 685 | 686 | lodash._baseassign@^3.0.0: 687 | version "3.2.0" 688 | resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" 689 | dependencies: 690 | lodash._basecopy "^3.0.0" 691 | lodash.keys "^3.0.0" 692 | 693 | lodash._basecopy@^3.0.0: 694 | version "3.0.1" 695 | resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" 696 | 697 | lodash._basecreate@^3.0.0: 698 | version "3.0.3" 699 | resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" 700 | 701 | lodash._getnative@^3.0.0: 702 | version "3.9.1" 703 | resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" 704 | 705 | lodash._isiterateecall@^3.0.0: 706 | version "3.0.9" 707 | resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" 708 | 709 | lodash.camelcase@^4.3.0: 710 | version "4.3.0" 711 | resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" 712 | 713 | lodash.create@3.1.1: 714 | version "3.1.1" 715 | resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" 716 | dependencies: 717 | lodash._baseassign "^3.0.0" 718 | lodash._basecreate "^3.0.0" 719 | lodash._isiterateecall "^3.0.0" 720 | 721 | lodash.get@^4.4.2: 722 | version "4.4.2" 723 | resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" 724 | 725 | lodash.isarguments@^3.0.0: 726 | version "3.1.0" 727 | resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" 728 | 729 | lodash.isarray@^3.0.0: 730 | version "3.0.4" 731 | resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" 732 | 733 | lodash.keys@^3.0.0: 734 | version "3.1.2" 735 | resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" 736 | dependencies: 737 | lodash._getnative "^3.0.0" 738 | lodash.isarguments "^3.0.0" 739 | lodash.isarray "^3.0.0" 740 | 741 | lodash.noop@^3.0.1: 742 | version "3.0.1" 743 | resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" 744 | 745 | lodash.pad@^4.1.0: 746 | version "4.5.1" 747 | resolved "https://registry.yarnpkg.com/lodash.pad/-/lodash.pad-4.5.1.tgz#4330949a833a7c8da22cc20f6a26c4d59debba70" 748 | 749 | lodash.padend@^4.1.0: 750 | version "4.6.1" 751 | resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" 752 | 753 | lodash.padstart@^4.1.0: 754 | version "4.6.1" 755 | resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b" 756 | 757 | lodash.snakecase@^4.1.1: 758 | version "4.1.1" 759 | resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" 760 | 761 | lodash@^4.14.0: 762 | version "4.17.4" 763 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 764 | 765 | lolex@^1.6.0: 766 | version "1.6.0" 767 | resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" 768 | 769 | lolex@^2.2.0: 770 | version "2.3.1" 771 | resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.3.1.tgz#3d2319894471ea0950ef64692ead2a5318cff362" 772 | 773 | md5@^2.1.0: 774 | version "2.2.1" 775 | resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" 776 | dependencies: 777 | charenc "~0.0.1" 778 | crypt "~0.0.1" 779 | is-buffer "~1.1.1" 780 | 781 | media-typer@0.3.0: 782 | version "0.3.0" 783 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 784 | 785 | merge-descriptors@1.0.1: 786 | version "1.0.1" 787 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 788 | 789 | methods@~1.1.2: 790 | version "1.1.2" 791 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 792 | 793 | mime-db@~1.30.0: 794 | version "1.30.0" 795 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" 796 | 797 | mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17: 798 | version "2.1.17" 799 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" 800 | dependencies: 801 | mime-db "~1.30.0" 802 | 803 | mime@1.4.1: 804 | version "1.4.1" 805 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" 806 | 807 | mime@^1.4.1: 808 | version "1.6.0" 809 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 810 | 811 | minimatch@^3.0.2, minimatch@^3.0.4: 812 | version "3.0.4" 813 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 814 | dependencies: 815 | brace-expansion "^1.1.7" 816 | 817 | minimist@0.0.8: 818 | version "0.0.8" 819 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 820 | 821 | mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@~0.5.1: 822 | version "0.5.1" 823 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 824 | dependencies: 825 | minimist "0.0.8" 826 | 827 | mkpath@^0.1.0: 828 | version "0.1.0" 829 | resolved "https://registry.yarnpkg.com/mkpath/-/mkpath-0.1.0.tgz#7554a6f8d871834cc97b5462b122c4c124d6de91" 830 | 831 | mocha-circleci-reporter@^0.0.2: 832 | version "0.0.2" 833 | resolved "https://registry.yarnpkg.com/mocha-circleci-reporter/-/mocha-circleci-reporter-0.0.2.tgz#6cbb3f1a1911ce2365e79461a156370b63790c7e" 834 | dependencies: 835 | mocha "^3.0.0" 836 | mocha-junit-reporter "^1.12.0" 837 | 838 | mocha-junit-reporter@^1.12.0: 839 | version "1.15.0" 840 | resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.15.0.tgz#309f4b7a20fcda26d0ad69c9b7d0808d772302c2" 841 | dependencies: 842 | debug "^2.2.0" 843 | md5 "^2.1.0" 844 | mkdirp "~0.5.1" 845 | xml "^1.0.0" 846 | 847 | mocha@^3.0.0: 848 | version "3.5.3" 849 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" 850 | dependencies: 851 | browser-stdout "1.3.0" 852 | commander "2.9.0" 853 | debug "2.6.8" 854 | diff "3.2.0" 855 | escape-string-regexp "1.0.5" 856 | glob "7.1.1" 857 | growl "1.9.2" 858 | he "1.1.1" 859 | json3 "3.3.2" 860 | lodash.create "3.1.1" 861 | mkdirp "0.5.1" 862 | supports-color "3.1.2" 863 | 864 | mocha@^4.1.0: 865 | version "4.1.0" 866 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794" 867 | dependencies: 868 | browser-stdout "1.3.0" 869 | commander "2.11.0" 870 | debug "3.1.0" 871 | diff "3.3.1" 872 | escape-string-regexp "1.0.5" 873 | glob "7.1.2" 874 | growl "1.10.3" 875 | he "1.1.1" 876 | mkdirp "0.5.1" 877 | supports-color "4.4.0" 878 | 879 | ms@2.0.0: 880 | version "2.0.0" 881 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 882 | 883 | negotiator@0.6.1: 884 | version "0.6.1" 885 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 886 | 887 | ngrok@^2.2.24: 888 | version "2.2.24" 889 | resolved "https://registry.yarnpkg.com/ngrok/-/ngrok-2.2.24.tgz#0fce41f4f60b1ba7484e052a7ed24868686bef53" 890 | dependencies: 891 | "@types/node" "^8.0.19" 892 | async "^2.3.0" 893 | decompress-zip "^0.3.0" 894 | lock "^0.1.2" 895 | request "^2.55.0" 896 | uuid "^3.0.0" 897 | 898 | nise@^1.2.0: 899 | version "1.2.0" 900 | resolved "https://registry.yarnpkg.com/nise/-/nise-1.2.0.tgz#079d6cadbbcb12ba30e38f1c999f36ad4d6baa53" 901 | dependencies: 902 | formatio "^1.2.0" 903 | just-extend "^1.1.26" 904 | lolex "^1.6.0" 905 | path-to-regexp "^1.7.0" 906 | text-encoding "^0.6.4" 907 | 908 | node-fetch@^1.7.3: 909 | version "1.7.3" 910 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" 911 | dependencies: 912 | encoding "^0.1.11" 913 | is-stream "^1.0.1" 914 | 915 | node-forge@^0.7.1: 916 | version "0.7.1" 917 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" 918 | 919 | nopt@^3.0.1: 920 | version "3.0.6" 921 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" 922 | dependencies: 923 | abbrev "1" 924 | 925 | nopt@~1.0.10: 926 | version "1.0.10" 927 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" 928 | dependencies: 929 | abbrev "1" 930 | 931 | "normalize-package-data@~1.0.1 || ^2.0.0": 932 | version "2.4.0" 933 | resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" 934 | dependencies: 935 | hosted-git-info "^2.1.4" 936 | is-builtin-module "^1.0.0" 937 | semver "2 || 3 || 4 || 5" 938 | validate-npm-package-license "^3.0.1" 939 | 940 | npm-cli-login@^0.0.10: 941 | version "0.0.10" 942 | resolved "https://registry.yarnpkg.com/npm-cli-login/-/npm-cli-login-0.0.10.tgz#b1e8b5b7405008e0a26ccc170443434a59c5364c" 943 | dependencies: 944 | npm-registry-client "7.0.9" 945 | 946 | "npm-package-arg@^3.0.0 || ^4.0.0": 947 | version "4.2.1" 948 | resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-4.2.1.tgz#593303fdea85f7c422775f17f9eb7670f680e3ec" 949 | dependencies: 950 | hosted-git-info "^2.1.5" 951 | semver "^5.1.0" 952 | 953 | npm-registry-client@7.0.9: 954 | version "7.0.9" 955 | resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-7.0.9.tgz#1baf86ee5285c4e6d38d4556208ded56049231bb" 956 | dependencies: 957 | chownr "^1.0.1" 958 | concat-stream "^1.4.6" 959 | graceful-fs "^4.1.2" 960 | mkdirp "^0.5.0" 961 | normalize-package-data "~1.0.1 || ^2.0.0" 962 | npm-package-arg "^3.0.0 || ^4.0.0" 963 | once "^1.3.0" 964 | request "^2.47.0" 965 | retry "^0.8.0" 966 | rimraf "2" 967 | semver "2 >=2.2.1 || 3.x || 4 || 5" 968 | slide "^1.1.3" 969 | optionalDependencies: 970 | npmlog "~2.0.0" 971 | 972 | npmlog@~2.0.0: 973 | version "2.0.4" 974 | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-2.0.4.tgz#98b52530f2514ca90d09ec5b22c8846722375692" 975 | dependencies: 976 | ansi "~0.3.1" 977 | are-we-there-yet "~1.1.2" 978 | gauge "~1.2.5" 979 | 980 | oauth-sign@~0.8.2: 981 | version "0.8.2" 982 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" 983 | 984 | on-finished@~2.3.0: 985 | version "2.3.0" 986 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 987 | dependencies: 988 | ee-first "1.1.1" 989 | 990 | once@^1.3.0: 991 | version "1.4.0" 992 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 993 | dependencies: 994 | wrappy "1" 995 | 996 | parseurl@~1.3.2: 997 | version "1.3.2" 998 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 999 | 1000 | path-is-absolute@^1.0.0: 1001 | version "1.0.1" 1002 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 1003 | 1004 | path-to-regexp@0.1.7: 1005 | version "0.1.7" 1006 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 1007 | 1008 | path-to-regexp@^1.7.0: 1009 | version "1.7.0" 1010 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" 1011 | dependencies: 1012 | isarray "0.0.1" 1013 | 1014 | performance-now@^2.1.0: 1015 | version "2.1.0" 1016 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 1017 | 1018 | process-nextick-args@~1.0.6: 1019 | version "1.0.7" 1020 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 1021 | 1022 | proxy-addr@~2.0.2: 1023 | version "2.0.2" 1024 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" 1025 | dependencies: 1026 | forwarded "~0.1.2" 1027 | ipaddr.js "1.5.2" 1028 | 1029 | punycode@^1.4.1: 1030 | version "1.4.1" 1031 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 1032 | 1033 | q@^1.1.2: 1034 | version "1.5.1" 1035 | resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" 1036 | 1037 | qs@6.5.1, qs@~6.5.1: 1038 | version "6.5.1" 1039 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 1040 | 1041 | range-parser@~1.2.0: 1042 | version "1.2.0" 1043 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 1044 | 1045 | raw-body@2.3.2: 1046 | version "2.3.2" 1047 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 1048 | dependencies: 1049 | bytes "3.0.0" 1050 | http-errors "1.6.2" 1051 | iconv-lite "0.4.19" 1052 | unpipe "1.0.0" 1053 | 1054 | readable-stream@^1.1.8: 1055 | version "1.1.14" 1056 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" 1057 | dependencies: 1058 | core-util-is "~1.0.0" 1059 | inherits "~2.0.1" 1060 | isarray "0.0.1" 1061 | string_decoder "~0.10.x" 1062 | 1063 | readable-stream@^2.0.6, readable-stream@^2.2.2: 1064 | version "2.3.3" 1065 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" 1066 | dependencies: 1067 | core-util-is "~1.0.0" 1068 | inherits "~2.0.3" 1069 | isarray "~1.0.0" 1070 | process-nextick-args "~1.0.6" 1071 | safe-buffer "~5.1.1" 1072 | string_decoder "~1.0.3" 1073 | util-deprecate "~1.0.1" 1074 | 1075 | request@^2.47.0, request@^2.55.0, request@^2.72.0, request@^2.74.0: 1076 | version "2.83.0" 1077 | resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" 1078 | dependencies: 1079 | aws-sign2 "~0.7.0" 1080 | aws4 "^1.6.0" 1081 | caseless "~0.12.0" 1082 | combined-stream "~1.0.5" 1083 | extend "~3.0.1" 1084 | forever-agent "~0.6.1" 1085 | form-data "~2.3.1" 1086 | har-validator "~5.0.3" 1087 | hawk "~6.0.2" 1088 | http-signature "~1.2.0" 1089 | is-typedarray "~1.0.0" 1090 | isstream "~0.1.2" 1091 | json-stringify-safe "~5.0.1" 1092 | mime-types "~2.1.17" 1093 | oauth-sign "~0.8.2" 1094 | performance-now "^2.1.0" 1095 | qs "~6.5.1" 1096 | safe-buffer "^5.1.1" 1097 | stringstream "~0.0.5" 1098 | tough-cookie "~2.3.3" 1099 | tunnel-agent "^0.6.0" 1100 | uuid "^3.1.0" 1101 | 1102 | retry@^0.8.0: 1103 | version "0.8.0" 1104 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.8.0.tgz#2367628dc0edb247b1eab649dc53ac8628ac2d5f" 1105 | 1106 | rimraf@2: 1107 | version "2.6.2" 1108 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" 1109 | dependencies: 1110 | glob "^7.0.5" 1111 | 1112 | rxjs@^5.0.3: 1113 | version "5.5.6" 1114 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.6.tgz#e31fb96d6fd2ff1fd84bcea8ae9c02d007179c02" 1115 | dependencies: 1116 | symbol-observable "1.0.1" 1117 | 1118 | safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 1119 | version "5.1.1" 1120 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 1121 | 1122 | samsam@1.x: 1123 | version "1.3.0" 1124 | resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" 1125 | 1126 | "semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", semver@^5.1.0: 1127 | version "5.4.1" 1128 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" 1129 | 1130 | send@0.16.1: 1131 | version "0.16.1" 1132 | resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" 1133 | dependencies: 1134 | debug "2.6.9" 1135 | depd "~1.1.1" 1136 | destroy "~1.0.4" 1137 | encodeurl "~1.0.1" 1138 | escape-html "~1.0.3" 1139 | etag "~1.8.1" 1140 | fresh "0.5.2" 1141 | http-errors "~1.6.2" 1142 | mime "1.4.1" 1143 | ms "2.0.0" 1144 | on-finished "~2.3.0" 1145 | range-parser "~1.2.0" 1146 | statuses "~1.3.1" 1147 | 1148 | serve-static@1.13.1: 1149 | version "1.13.1" 1150 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" 1151 | dependencies: 1152 | encodeurl "~1.0.1" 1153 | escape-html "~1.0.3" 1154 | parseurl "~1.3.2" 1155 | send "0.16.1" 1156 | 1157 | setprototypeof@1.0.3: 1158 | version "1.0.3" 1159 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 1160 | 1161 | setprototypeof@1.1.0: 1162 | version "1.1.0" 1163 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 1164 | 1165 | sinon@^4.1.3: 1166 | version "4.1.3" 1167 | resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.1.3.tgz#fc599eda47ed9f1a694ce774b94ab44260bd7ac5" 1168 | dependencies: 1169 | diff "^3.1.0" 1170 | formatio "1.2.0" 1171 | lodash.get "^4.4.2" 1172 | lolex "^2.2.0" 1173 | nise "^1.2.0" 1174 | supports-color "^4.4.0" 1175 | type-detect "^4.0.5" 1176 | 1177 | slide@^1.1.3: 1178 | version "1.1.6" 1179 | resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" 1180 | 1181 | sntp@2.x.x: 1182 | version "2.1.0" 1183 | resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" 1184 | dependencies: 1185 | hoek "4.x.x" 1186 | 1187 | spdx-correct@~1.0.0: 1188 | version "1.0.2" 1189 | resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" 1190 | dependencies: 1191 | spdx-license-ids "^1.0.2" 1192 | 1193 | spdx-expression-parse@~1.0.0: 1194 | version "1.0.4" 1195 | resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" 1196 | 1197 | spdx-license-ids@^1.0.2: 1198 | version "1.2.2" 1199 | resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" 1200 | 1201 | sshpk@^1.7.0: 1202 | version "1.13.1" 1203 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" 1204 | dependencies: 1205 | asn1 "~0.2.3" 1206 | assert-plus "^1.0.0" 1207 | dashdash "^1.12.0" 1208 | getpass "^0.1.1" 1209 | optionalDependencies: 1210 | bcrypt-pbkdf "^1.0.0" 1211 | ecc-jsbn "~0.1.1" 1212 | jsbn "~0.1.0" 1213 | tweetnacl "~0.14.0" 1214 | 1215 | "statuses@>= 1.3.1 < 2": 1216 | version "1.4.0" 1217 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" 1218 | 1219 | statuses@~1.3.1: 1220 | version "1.3.1" 1221 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 1222 | 1223 | string_decoder@~0.10.x: 1224 | version "0.10.31" 1225 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 1226 | 1227 | string_decoder@~1.0.3: 1228 | version "1.0.3" 1229 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 1230 | dependencies: 1231 | safe-buffer "~5.1.0" 1232 | 1233 | stringstream@~0.0.5: 1234 | version "0.0.5" 1235 | resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" 1236 | 1237 | supports-color@3.1.2: 1238 | version "3.1.2" 1239 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" 1240 | dependencies: 1241 | has-flag "^1.0.0" 1242 | 1243 | supports-color@4.4.0: 1244 | version "4.4.0" 1245 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" 1246 | dependencies: 1247 | has-flag "^2.0.0" 1248 | 1249 | supports-color@^4.4.0: 1250 | version "4.5.0" 1251 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" 1252 | dependencies: 1253 | has-flag "^2.0.0" 1254 | 1255 | symbol-observable@1.0.1: 1256 | version "1.0.1" 1257 | resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" 1258 | 1259 | text-encoding@^0.6.4: 1260 | version "0.6.4" 1261 | resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" 1262 | 1263 | touch@0.0.3: 1264 | version "0.0.3" 1265 | resolved "https://registry.yarnpkg.com/touch/-/touch-0.0.3.tgz#51aef3d449571d4f287a5d87c9c8b49181a0db1d" 1266 | dependencies: 1267 | nopt "~1.0.10" 1268 | 1269 | tough-cookie@~2.3.3: 1270 | version "2.3.3" 1271 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" 1272 | dependencies: 1273 | punycode "^1.4.1" 1274 | 1275 | "traverse@>=0.3.0 <0.4": 1276 | version "0.3.9" 1277 | resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" 1278 | 1279 | tunnel-agent@^0.6.0: 1280 | version "0.6.0" 1281 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 1282 | dependencies: 1283 | safe-buffer "^5.0.1" 1284 | 1285 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 1286 | version "0.14.5" 1287 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 1288 | 1289 | type-detect@^4.0.5: 1290 | version "4.0.5" 1291 | resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2" 1292 | 1293 | type-is@~1.6.15: 1294 | version "1.6.15" 1295 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 1296 | dependencies: 1297 | media-typer "0.3.0" 1298 | mime-types "~2.1.15" 1299 | 1300 | typedarray@^0.0.6: 1301 | version "0.0.6" 1302 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 1303 | 1304 | unpipe@1.0.0, unpipe@~1.0.0: 1305 | version "1.0.0" 1306 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1307 | 1308 | util-deprecate@~1.0.1: 1309 | version "1.0.2" 1310 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1311 | 1312 | utils-merge@1.0.1: 1313 | version "1.0.1" 1314 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1315 | 1316 | uuid@^3.0.0, uuid@^3.1.0: 1317 | version "3.1.0" 1318 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" 1319 | 1320 | validate-npm-package-license@^3.0.1: 1321 | version "3.0.1" 1322 | resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" 1323 | dependencies: 1324 | spdx-correct "~1.0.0" 1325 | spdx-expression-parse "~1.0.0" 1326 | 1327 | vary@~1.1.2: 1328 | version "1.1.2" 1329 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1330 | 1331 | verror@1.10.0: 1332 | version "1.10.0" 1333 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 1334 | dependencies: 1335 | assert-plus "^1.0.0" 1336 | core-util-is "1.0.2" 1337 | extsprintf "^1.2.0" 1338 | 1339 | wrappy@1: 1340 | version "1.0.2" 1341 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1342 | 1343 | xhr2@^0.1.4: 1344 | version "0.1.4" 1345 | resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f" 1346 | 1347 | xml@^1.0.0: 1348 | version "1.0.1" 1349 | resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" 1350 | --------------------------------------------------------------------------------