diff --git a/package.json b/package.json index eedb60485..0d3c7a332 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "typescript": "^2.7.2", "url-loader": "^1.0.1", "uuid": "^3.2.1", + "web-push": "^3.3.2", "webdriverio": "^4.12.0", "webpack": "^4.10.2", "webpack-cli": "^2.1.4", diff --git a/src/constants.js b/src/constants.js index 3d293fbee..ef3d4fbf0 100644 --- a/src/constants.js +++ b/src/constants.js @@ -30,6 +30,7 @@ exports.RULES_PATH = '/rules'; exports.OAUTH_PATH = '/oauth'; exports.OAUTHCLIENTS_PATH = '/authorizations'; exports.LOGS_PATH = '/logs'; +exports.PUSH_PATH = '/push'; // Remember we end up in the build/* directory so these paths looks slightly // different than you might expect. exports.STATIC_PATH = path.join(__dirname, '../static'); diff --git a/src/controllers/push_controller.js b/src/controllers/push_controller.js new file mode 100644 index 000000000..550e3b0c5 --- /dev/null +++ b/src/controllers/push_controller.js @@ -0,0 +1,60 @@ +/** + * Push API Controller. + * + * Implements the Push API for notifications to use + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +const PromiseRouter = require('express-promise-router'); +const WebPush = require('web-push'); +const Database = require('../db'); +const Settings = require('../models/settings'); + +const PushController = PromiseRouter(); + +/** + * Initialize the Push API, generating and storing a VAPID keypair if necessary + */ +PushController.init = async () => { + let vapid = await Settings.get('push.vapid'); + if (!vapid) { + vapid = WebPush.generateVAPIDKeys(); + await Settings.set('push.vapid', vapid); + } + const {publicKey, privateKey} = vapid; + + const {tunnelDomain} = await Settings.getTunnelInfo(); + WebPush.setVapidDetails(tunnelDomain, publicKey, privateKey); +}; + +/** + * Handle requests for the public key + */ +PushController.get('/vapid-public-key', async (request, response) => { + const vapid = await Settings.get('push.vapid'); + if (!vapid) { + response.status(500).json({error: 'vapid not configured'}); + return; + } + response.status(200).json({publicKey: vapid.publicKey}); +}); + +PushController.post('/register', async (request, response) => { + await Database.createPushSubscription(request.body.subscription); + response.status(200).json({}); +}); + +PushController.broadcastNotification = async (message) => { + const subscriptions = await Database.getPushSubscriptions(); + for (const subscription of subscriptions) { + WebPush.sendNotification(subscription, message).catch((err) => { + console.warn('Push API error', err); + Database.deletePushSubscription(subscription.id); + }); + } +}; + +module.exports = PushController; diff --git a/src/controllers/settings_controller.js b/src/controllers/settings_controller.js index a320d36e6..56d7acb20 100644 --- a/src/controllers/settings_controller.js +++ b/src/controllers/settings_controller.js @@ -239,48 +239,7 @@ SettingsController.post('/skiptunnel', async (request, response) => { SettingsController.get('/tunnelinfo', async (request, response) => { try { - // Check to see if we have a tunnel endpoint first - const result = await Settings.get('tunneltoken'); - let localDomain; - let mDNSstate; - let tunnelEndpoint; - - if (typeof result === 'object') { - console.log(`Tunnel domain found. Tunnel name is: ${result.name} and`, - `tunnel domain is: ${config.get('ssltunnel.domain')}`); - tunnelEndpoint = - `https://${result.name}.${config.get('ssltunnel.domain')}`; - } else { - tunnelEndpoint = 'Not set.'; - } - - // Find out our default local DNS name Check for a previous name in the - // DB, if that does not exist use the default. - try { - mDNSstate = await Settings.get('multicastDNSstate'); - localDomain = await Settings.get('localDNSname'); - // If our DB is empty use defaults - if (typeof mDNSstate === 'undefined') { - mDNSstate = config.get( - 'settings.defaults.domain.localAccess'); - } - if (typeof localDomain === 'undefined') { - localDomain = config.get( - 'settings.defaults.domain.localControl.mdnsServiceDomain'); - } - } catch (err) { - // Catch this DB error. Since we don't know what state the mDNS process - // should be in make sure it's off - console.error(`Error getting DB entry for multicast from the DB: ${err}`); - localDomain = config.get( - 'settings.defaults.domain.localControl.mdnsServiceDomain'); - } - - console.log(`Tunnel name is set to: ${tunnelEndpoint}`); - console.log(`Local mDNS Service Domain Name is: ${localDomain}`); - const localDomainSettings = {localDomain: localDomain, - mDNSstate: mDNSstate, - tunnelDomain: tunnelEndpoint}; + const localDomainSettings = await Settings.getTunnelInfo(); response.send(localDomainSettings); response.status(200).end(); } catch (e) { diff --git a/src/db.js b/src/db.js index 18a93224b..93180b7ce 100644 --- a/src/db.js +++ b/src/db.js @@ -25,6 +25,7 @@ const TABLES = [ 'jsonwebtokens', 'things', 'settings', + 'pushSubscriptions', ]; const DEBUG = false || (process.env.NODE_ENV === 'test'); @@ -118,6 +119,11 @@ const Database = { 'key TEXT PRIMARY KEY,' + 'value TEXT' + ');'); + + this.db.run(`CREATE TABLE IF NOT EXISTS pushSubscriptions ( + id INTEGER PRIMARY KEY, + subscription TEXT + );`); }, /** @@ -526,6 +532,54 @@ const Database = { return result.changes !== 0; }, + /** + * Store a new Push subscription + * @param {Object} subscription + * @return {Promise} resolves to sub id + */ + createPushSubscription: function(desc) { + return this.run( + 'INSERT INTO pushSubscriptions (subscription) VALUES (?)', + [JSON.stringify(desc)] + ).then((res) => { + return parseInt(res.lastID); + }); + }, + + /** + * Get all push subscriptions + * @return {Promise>} + */ + getPushSubscriptions: function() { + return new Promise((resolve, reject) => { + this.db.all( + 'SELECT id, subscription FROM pushSubscriptions', + [], + function(err, rows) { + if (err) { + reject(err); + return; + } + const subs = []; + for (const row of rows) { + const sub = JSON.parse(row.subscription); + sub.id = row.id; + subs.push(sub); + } + resolve(subs); + } + ); + }); + }, + + /** + * Delete a single subscription + * @param {number} id + */ + deletePushSubscription: function(id) { + return this.run('DELETE FROM pushSubscriptions WHERE id = ?', [id]); + }, + /** * ONLY for tests (clears all tables). */ diff --git a/src/models/settings.js b/src/models/settings.js index 1b7fedba8..204825e49 100644 --- a/src/models/settings.js +++ b/src/models/settings.js @@ -10,6 +10,7 @@ 'use strict'; +const config = require('config'); const Database = require('../db'); const util = require('util'); @@ -70,6 +71,57 @@ const Settings = { throw e; }); }, + + /** + * Get an object of all tunnel settings + * @return {localDomain, mDNSstate, tunnelDomain} + */ + getTunnelInfo: async function() { + // Check to see if we have a tunnel endpoint first + const result = await Settings.get('tunneltoken'); + let localDomain; + let mDNSstate; + let tunnelEndpoint; + + if (typeof result === 'object') { + console.log(`Tunnel domain found. Tunnel name is: ${result.name} and`, + `tunnel domain is: ${config.get('ssltunnel.domain')}`); + tunnelEndpoint = + `https://${result.name}.${config.get('ssltunnel.domain')}`; + } else { + tunnelEndpoint = 'Not set.'; + } + + // Find out our default local DNS name Check for a previous name in the + // DB, if that does not exist use the default. + try { + mDNSstate = await Settings.get('multicastDNSstate'); + localDomain = await Settings.get('localDNSname'); + // If our DB is empty use defaults + if (typeof mDNSstate === 'undefined') { + mDNSstate = config.get( + 'settings.defaults.domain.localAccess'); + } + if (typeof localDomain === 'undefined') { + localDomain = config.get( + 'settings.defaults.domain.localControl.mdnsServiceDomain'); + } + } catch (err) { + // Catch this DB error. Since we don't know what state the mDNS process + // should be in make sure it's off + console.error(`Error getting DB entry for multicast from the DB: ${err}`); + localDomain = config.get( + 'settings.defaults.domain.localControl.mdnsServiceDomain'); + } + + console.log(`Tunnel name is set to: ${tunnelEndpoint}`); + console.log(`Local mDNS Service Domain Name is: ${localDomain}`); + return { + localDomain: localDomain, + mDNSstate: mDNSstate, + tunnelDomain: tunnelEndpoint, + }; + }, }; module.exports = Settings; diff --git a/src/router.js b/src/router.js index 1057b9688..4d17ba352 100644 --- a/src/router.js +++ b/src/router.js @@ -128,6 +128,11 @@ const Router = { app.use(API_PREFIX + Constants.LOGS_PATH, nocache, auth, require('./controllers/logs_controller')); + const PushController = require('./controllers/push_controller'); + PushController.init().then(() => { + app.use(API_PREFIX + Constants.PUSH_PATH, nocache, auth, PushController); + }); + app.use(API_PREFIX + Constants.OAUTH_PATH, nocache, require('./controllers/oauth_controller').default); app.use(API_PREFIX + Constants.OAUTHCLIENTS_PATH, nocache, auth, diff --git a/src/rules-engine/effects/NotificationEffect.js b/src/rules-engine/effects/NotificationEffect.js new file mode 100644 index 000000000..47e28e5d3 --- /dev/null +++ b/src/rules-engine/effects/NotificationEffect.js @@ -0,0 +1,51 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/.* + */ + +const assert = require('assert'); +const Effect = require('./Effect'); +const PushController = require('../../controllers/push_controller'); + +/** + * An Effect which creates a notification + */ +class NotificationEffect extends Effect { + /** + * @param {EffectDescription} desc + */ + constructor(desc) { + super(desc); + + assert(desc.hasOwnProperty('message')); + + this.message = desc.message; + } + + /** + * @return {EffectDescription} + */ + toDescription() { + return Object.assign( + super.toDescription(), + { + message: this.message, + } + ); + } + + /** + * @param {State} state + */ + setState(state) { + if (!state.on) { + return; + } + + PushController.broadcastNotification(this.message); + } +} + +module.exports = NotificationEffect; + diff --git a/src/rules-engine/effects/index.js b/src/rules-engine/effects/index.js index 6d84c8d91..775697347 100644 --- a/src/rules-engine/effects/index.js +++ b/src/rules-engine/effects/index.js @@ -8,6 +8,7 @@ const effects = { Effect: require('./Effect'), ActionEffect: require('./ActionEffect'), MultiEffect: require('./MultiEffect'), + NotificationEffect: require('./NotificationEffect'), SetEffect: require('./SetEffect'), PulseEffect: require('./PulseEffect'), }; diff --git a/static/css/rule.css b/static/css/rule.css index 08cc21b2d..1b99e6274 100644 --- a/static/css/rule.css +++ b/static/css/rule.css @@ -185,7 +185,7 @@ white-space: nowrap; } -.property-select-option { +.property-select-option, .message-input-container { background: #4a4a4a; height: 5rem; line-height: 5rem; @@ -198,14 +198,18 @@ cursor: default; } -.property-select-name { +.property-select-name, .message-input-label { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; flex: 1; } -.property-select-option.selected { +.message-input-label { + overflow: visible; +} + +.property-select-option.selected, .message-input-container { position: relative; border-radius: 0 0 1rem 0; } @@ -237,7 +241,8 @@ } .property-select.open > .property-select-option, -.property-select-option.selected { +.property-select-option.selected, +.message-input-container { display: flex; align-items: center; } @@ -247,11 +252,13 @@ } .property-select-option select, -.property-select-option input { +.property-select-option input, +.message-input-container input { margin-left: 0.6rem; } -.property-select-option input[type="text"] { +.property-select-option input[type="text"], +.message-input-container input[type="text"] { width: 8rem; } diff --git a/static/images/rule-icons/notification.svg b/static/images/rule-icons/notification.svg new file mode 100644 index 000000000..0145163a5 --- /dev/null +++ b/static/images/rule-icons/notification.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/static/js/app.js b/static/js/app.js index 9c813ddc4..361410bef 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -36,6 +36,8 @@ let RuleScreen; // eslint-disable-next-line prefer-const let Speech; +const Notifications = require('./notifications'); + const App = { /** * Current server host. @@ -307,6 +309,7 @@ if (navigator.serviceWorker) { navigator.serviceWorker.register('/service-worker.js', { scope: '/', }); + navigator.serviceWorker.ready.then(Notifications.onReady); } /** diff --git a/static/js/notifications.js b/static/js/notifications.js new file mode 100644 index 000000000..597d361b6 --- /dev/null +++ b/static/js/notifications.js @@ -0,0 +1,61 @@ +/** + * Notifications + * + * Implements a basic form of the Push API. Based on the + * serviceworker cookbook payload example. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +'use strict'; + +const API = require('./api'); + +const Notifications = { + onReady: async function(registration) { + let subscription = await registration.pushManager.getSubscription(); + if (!subscription) { + const res = await fetch('/push/vapid-public-key', { + headers: API.headers(), + }); + const vapid = await res.json(); + const convertedVapidKey = urlBase64ToUint8Array(vapid.publicKey); + subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: convertedVapidKey, + }); + } + await fetch('/push/register', { + method: 'post', + headers: Object.assign({ + 'Content-Type': 'application/json', + }, API.headers()), + body: JSON.stringify({ + subscription: subscription, + }), + }); + }, +}; + +// From https://github.com/mozilla/serviceworker-cookbook/blob/master/tools.js +// This function is needed because Chrome doesn't accept a base64 encoded string +// as value for applicationServerKey in pushManager.subscribe yet +// https://bugs.chromium.org/p/chromium/issues/detail?id=802280 +function urlBase64ToUint8Array(base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} + + +module.exports = Notifications; diff --git a/static/js/rule-screen.js b/static/js/rule-screen.js index 4c87f94fd..dfc2458ac 100644 --- a/static/js/rule-screen.js +++ b/static/js/rule-screen.js @@ -14,6 +14,7 @@ const DevicePropertyBlock = require('./rules/DevicePropertyBlock'); const Gateway = require('./rules/Gateway'); const Rule = require('./rules/Rule'); const RuleUtils = require('./rules/RuleUtils'); +const NotificationEffectBlock = require('./rules/NotificationEffectBlock'); const TimeTriggerBlock = require('./rules/TimeTriggerBlock'); const page = require('page'); @@ -181,6 +182,26 @@ const RuleScreen = { this.partBlocks.push(newBlock); }, + /** + * Instantiate a draggable NotificationEffectBlock from a template + * NotificationEffectBlock in the palette + * @param {Event} event + */ + onNotificationEffectBlockDown: function(event) { + if (!this.rule) { + return; + } + const deviceRect = event.target.getBoundingClientRect(); + const x = deviceRect.left; + const y = deviceRect.top; + + const newBlock = new NotificationEffectBlock( + this.ruleArea, this.onPresentationChange, this.onRuleChange); + newBlock.snapToGrid(x, y); + newBlock.draggable.onDown(event); + this.partBlocks.push(newBlock); + }, + /** * Create a block representing a time trigger * @return {Element} @@ -197,6 +218,22 @@ const RuleScreen = { return elt; }, + /** + * Create a block representing a notification effect + * @return {Element} + */ + makeNotificationEffectBlock: function() { + const elt = document.createElement('div'); + elt.classList.add('rule-part'); + + elt.innerHTML = `
+ +
+

Notification

`; + + return elt; + }, + /** * Create a device-block from a thing * @param {ThingDescription} thing @@ -224,6 +261,10 @@ const RuleScreen = { if (part.type === 'TimeTrigger') { block = new TimeTriggerBlock(this.ruleArea, this.onPresentationChange, this.onRuleChange); + } else if (part.type === 'NotificationEffect') { + block = new NotificationEffectBlock(this.ruleArea, + this.onPresentationChange, + this.onRuleChange); } else { const thing = RuleUtils.thingFromPart(this.gateway, part); if (!thing) { @@ -248,7 +289,8 @@ const RuleScreen = { function isValidSelection(block) { const selectedOption = block.querySelector('.selected'); if (!selectedOption) { - return !!block.querySelector('.time-input'); + return block.querySelector('.time-input') || + block.querySelector('.message-input-container'); } return JSON.parse(selectedOption.dataset.ruleFragment); } @@ -526,6 +568,13 @@ const RuleScreen = { this.onTimeTriggerBlockDown.bind(this)); this.rulePartsList.appendChild(ttBlock); + const neBlock = this.makeNotificationEffectBlock(); + neBlock.addEventListener('mousedown', + this.onNotificationEffectBlockDown.bind(this)); + neBlock.addEventListener('touchstart', + this.onNotificationEffectBlockDown.bind(this)); + this.rulePartsList.appendChild(neBlock); + this.gateway.readThings().then((things) => { for (const thing of things) { const elt = this.makeDeviceBlock(thing); @@ -769,4 +818,6 @@ const RuleScreen = { }, }; +window.RuleScreen = RuleScreen; + module.exports = RuleScreen; diff --git a/static/js/rules/NotificationEffectBlock.js b/static/js/rules/NotificationEffectBlock.js new file mode 100644 index 000000000..546825920 --- /dev/null +++ b/static/js/rules/NotificationEffectBlock.js @@ -0,0 +1,84 @@ +const RulePartBlock = require('./RulePartBlock'); + +/** + * An element representing a notification effect + * + * @constructor + * @param {Element} ruleArea + * @param {Function} onPresentationChange + * @param {Function} onRuleChange + */ +function NotificationEffectBlock(ruleArea, onPresentationChange, onRuleUpdate) { + RulePartBlock.call(this, ruleArea, onPresentationChange, onRuleUpdate, + 'Notification', + '/optimized-images/rule-icons/notification.svg'); + + const rulePartInfo = this.elt.querySelector('.rule-part-info'); + + const messageInputContainer = document.createElement('div'); + messageInputContainer.classList.add('message-input-container'); + + const label = document.createElement('span'); + label.classList.add('message-input-label'); + label.textContent = 'Message'; + messageInputContainer.appendChild(label); + + this.messageInput = document.createElement('input'); + this.messageInput.type = 'text'; + + messageInputContainer.appendChild(this.messageInput); + + // Disable dragging started by clicking input + this.messageInput.addEventListener('mousedown', (e) => { + e.stopPropagation(); + }); + this.messageInput.addEventListener('touchstart', (e) => { + e.stopPropagation(); + }); + + this.messageInput.addEventListener('change', () => { + this.rulePart = {effect: { + type: 'NotificationEffect', + message: this.messageInput.value, + }}; + this.onRuleChange(); + }); + + rulePartInfo.appendChild(messageInputContainer); +} + +NotificationEffectBlock.prototype = Object.create(RulePartBlock.prototype); + +/** + * Initialize based on an existing partial rule + */ +NotificationEffectBlock.prototype.setRulePart = function(rulePart) { + this.rulePart = rulePart; + + if (rulePart.effect) { + this.role = 'effect'; + this.rulePartBlock.classList.add('effect'); + + this.messageInput.value = rulePart.effect.message; + } + + if (rulePart.trigger) { + throw new Error('NotificationEffectBlock can only be an effect'); + } +}; + +NotificationEffectBlock.prototype.onUp = function(clientX, clientY) { + RulePartBlock.prototype.onUp.call(this, clientX, clientY); + if (this.role === 'trigger') { + this.remove(); + } + if (this.role === 'effect') { + this.rulePart = {effect: { + type: 'NotificationEffect', + message: this.messageInput.value, + }}; + this.onRuleChange(); + } +}; + +module.exports = NotificationEffectBlock; diff --git a/static/js/rules/Rule.js b/static/js/rules/Rule.js index 1617876e5..385c5cfa5 100644 --- a/static/js/rules/Rule.js +++ b/static/js/rules/Rule.js @@ -227,6 +227,9 @@ Rule.prototype.singleEffectToHumanRepresentation = function(effect) { return effectStr; } + if (effect.type === 'NotificationEffect') { + return `notify with message "${effect.message}"`; + } if (effect.type === 'ActionEffect') { const effectThing = this.gateway.things.filter( RuleUtils.byHref(effect.thing.href) @@ -296,7 +299,7 @@ Rule.prototype.toHumanRepresentation = function(html) { const effectExists = this.effect && this.effect.effects && this.effect.effects.length > 0; - let permanent = !effectExists; // Default to permanent + let permanent = true; // Default to permanent if (effectExists) { for (const effect of this.effect.effects) { if (effect.type === 'SetEffect') { diff --git a/static/js/rules/RuleUtils.js b/static/js/rules/RuleUtils.js index 9d37b8229..0a790b546 100644 --- a/static/js/rules/RuleUtils.js +++ b/static/js/rules/RuleUtils.js @@ -52,6 +52,10 @@ const RuleUtils = { // Helper function for selecting the thing corresponding to a property byProperty: function byProperty(property) { return function(option) { + if (!property) { + console.warn('byProperty property undefined', new Error().stack); + return false; + } const optProp = option.properties[property.name]; return optProp && (optProp.href === property.href); }; diff --git a/static/optimized-images/rule-icons/notification.svg b/static/optimized-images/rule-icons/notification.svg new file mode 100644 index 000000000..7ad65fef4 --- /dev/null +++ b/static/optimized-images/rule-icons/notification.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/service-worker.js b/static/service-worker.js index fbad8a3a7..151e781e9 100644 --- a/static/service-worker.js +++ b/static/service-worker.js @@ -42,3 +42,11 @@ self.addEventListener('fetch', function(event) { } })()); }); + +self.addEventListener('push', function(event) { + const payload = event.data ? event.data.text() : ''; + + event.waitUntil(self.registration.showNotification('Mozilla IoT Gateway', { + body: payload, + })); +}); diff --git a/yarn.lock b/yarn.lock index 79c54ae50..c996c0c10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -904,6 +904,12 @@ acorn@^5.3.0, acorn@^5.5.0: version "5.5.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" +agent-base@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + dependencies: + es6-promisify "^5.0.0" + ajv-keywords@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" @@ -1168,7 +1174,7 @@ asn1.js@^4.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -asn1.js@^5.0.1: +asn1.js@^5.0.0, asn1.js@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.0.1.tgz#7668b56416953f0ce3421adbb3893ace59c96f59" dependencies: @@ -3682,10 +3688,20 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" +es6-promise@^4.0.3: + version "4.2.5" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" + es6-promise@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + dependencies: + es6-promise "^4.0.3" + es6-templates@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/es6-templates/-/es6-templates-0.2.3.tgz#5cb9ac9fb1ded6eb1239342b81d792bbb4078ee4" @@ -4964,10 +4980,23 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http_ece@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.0.5.tgz#b60660faaf14215102d1493ea720dcd92b53372f" + dependencies: + urlsafe-base64 "~1.0.0" + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" +https-proxy-agent@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" + dependencies: + agent-base "^4.1.0" + debug "^3.1.0" + iconv-lite@0.4.13: version "0.4.13" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" @@ -6076,7 +6105,7 @@ jwa@^1.1.5: ecdsa-sig-formatter "1.0.10" safe-buffer "^5.0.1" -jws@^3.1.5: +jws@^3.1.3, jws@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" dependencies: @@ -9891,6 +9920,10 @@ urlgrey@0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/urlgrey/-/urlgrey-0.4.4.tgz#892fe95960805e85519f1cd4389f2cb4cbb7652f" +urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6" + ursa@^0.9.4: version "0.9.4" resolved "https://registry.yarnpkg.com/ursa/-/ursa-0.9.4.tgz#0a2abfb7dc4267f733b0f8f2fc7f2c895d40a413" @@ -10041,6 +10074,17 @@ wdio-dot-reporter@~0.0.8: version "0.0.9" resolved "https://registry.yarnpkg.com/wdio-dot-reporter/-/wdio-dot-reporter-0.0.9.tgz#929b2adafd49d6b0534fda068e87319b47e38fe5" +web-push@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.3.2.tgz#7a8f8b77c8cb1875b02a53e45d7bc277a3d05368" + dependencies: + asn1.js "^5.0.0" + http_ece "1.0.5" + https-proxy-agent "^2.2.1" + jws "^3.1.3" + minimist "^1.2.0" + urlsafe-base64 "^1.0.0" + webdriverio@^4.12.0: version "4.12.0" resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-4.12.0.tgz#e340def272183c8168a4dd0b382322f9d7bee10d"