diff --git a/.travis.yml b/.travis.yml index 67382e760..823517fc5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js node_js: - - node # Latest stable version of nodejs. + - "10.11.0" script: - ./travis.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index d96c82612..4185005ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,329 @@ +Changes in [0.14.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.2) (2018-12-10) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.2-rc.1...v0.14.2) + + * No changes since rc.1 + +Changes in [0.14.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.2-rc.1) (2018-12-06) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.1...v0.14.2-rc.1) + + * fix some assertions in e2e backup unit test + [\#794](https://github.com/matrix-org/matrix-js-sdk/pull/794) + * Config should be called with auth + [\#798](https://github.com/matrix-org/matrix-js-sdk/pull/798) + * Don't re-establish sessions with unknown devices + [\#792](https://github.com/matrix-org/matrix-js-sdk/pull/792) + * e2e key backups + [\#684](https://github.com/matrix-org/matrix-js-sdk/pull/684) + * WIP: online incremental megolm backups + [\#595](https://github.com/matrix-org/matrix-js-sdk/pull/595) + * Support for e2e key backups + [\#736](https://github.com/matrix-org/matrix-js-sdk/pull/736) + * Passphrase Support for e2e backups + [\#786](https://github.com/matrix-org/matrix-js-sdk/pull/786) + * Add 'getSsoLoginUrl' function + [\#783](https://github.com/matrix-org/matrix-js-sdk/pull/783) + * Fix: don't set the room name to null when heroes are missing. + [\#784](https://github.com/matrix-org/matrix-js-sdk/pull/784) + * Handle crypto db version upgrades + [\#785](https://github.com/matrix-org/matrix-js-sdk/pull/785) + * Restart broken Olm sessions + [\#780](https://github.com/matrix-org/matrix-js-sdk/pull/780) + * Use the last olm session that got a message + [\#776](https://github.com/matrix-org/matrix-js-sdk/pull/776) + +Changes in [0.14.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.1) (2018-11-22) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.0...v0.14.1) + + * Warning when crypto DB is too new to use. + +Changes in [0.14.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.0) (2018-11-19) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.0-rc.1...v0.14.0) + + * No changes since rc.1 + +Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.0-rc.1) (2018-11-15) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.13.1...v0.14.0-rc.1) + +BREAKING CHANGE +---------------- + + * js-sdk now uses Olm 3.0. Apps using Olm must update to 3.0 to + continue using Olm with the js-sdk. The js-sdk will call Olm's + init() method when the client is started. + +All Changes +----------- + + * Prevent messages from being sent if other messages have failed to send + [\#781](https://github.com/matrix-org/matrix-js-sdk/pull/781) + * A unit test for olm + [\#777](https://github.com/matrix-org/matrix-js-sdk/pull/777) + * Set access_token and user_id after login in with username and password. + [\#778](https://github.com/matrix-org/matrix-js-sdk/pull/778) + * Add function to get currently joined rooms. + [\#779](https://github.com/matrix-org/matrix-js-sdk/pull/779) + * Remove the request-only stuff we don't need anymore + [\#775](https://github.com/matrix-org/matrix-js-sdk/pull/775) + * Manually construct query strings for browser-request instances + [\#770](https://github.com/matrix-org/matrix-js-sdk/pull/770) + * Fix: correctly check for crypto being present + [\#769](https://github.com/matrix-org/matrix-js-sdk/pull/769) + * Update babel-eslint to 8.1.1 + [\#768](https://github.com/matrix-org/matrix-js-sdk/pull/768) + * Support `request` in the browser and support supplying servers to try in + joinRoom() + [\#764](https://github.com/matrix-org/matrix-js-sdk/pull/764) + * loglevel should be a normal dependency + [\#767](https://github.com/matrix-org/matrix-js-sdk/pull/767) + * Stop devicelist when client is stopped + [\#766](https://github.com/matrix-org/matrix-js-sdk/pull/766) + * Update to WebAssembly-powered Olm + [\#743](https://github.com/matrix-org/matrix-js-sdk/pull/743) + * Logging lib. Fixes #332 + [\#763](https://github.com/matrix-org/matrix-js-sdk/pull/763) + * Use new stop() method on matrix-mock-request + [\#765](https://github.com/matrix-org/matrix-js-sdk/pull/765) + +Changes in [0.13.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.1) (2018-11-14) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.13.0...v0.13.1) + + * Add function to get currently joined rooms. + [\#779](https://github.com/matrix-org/matrix-js-sdk/pull/779) + +Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.0) (2018-11-15) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1...v0.13.0) + +BREAKING CHANGE +---------------- + * `MatrixClient::login` now sets client `access_token` and `user_id` following successful login with username and password. + +Changes in [0.12.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1) (2018-10-29) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1-rc.1...v0.12.1) + + * No changes since rc.1 + +Changes in [0.12.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1-rc.1) (2018-10-24) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0...v0.12.1-rc.1) + + * Add repository type to package.json to make it valid + [\#762](https://github.com/matrix-org/matrix-js-sdk/pull/762) + * Add getMediaConfig() + [\#761](https://github.com/matrix-org/matrix-js-sdk/pull/761) + * add new examples, to be expanded into a post + [\#739](https://github.com/matrix-org/matrix-js-sdk/pull/739) + +Changes in [0.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0) (2018-10-16) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0-rc.1...v0.12.0) + + * No changes since rc.1 + +Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0-rc.1) (2018-10-11) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1...v0.12.0-rc.1) + +BREAKING CHANGES +---------------- + * If js-sdk finds data in the store that is incompatible with the options currently being used, + it will emit sync state ERROR with an error of type InvalidStoreError. It will also stop trying + to sync in this situation: the app must stop the client and then either clear the store or + change the options (in this case, enable or disable lazy loading of members) and then start + the client again. + +All Changes +----------- + + * never replace /sync'ed memberships with OOB ones + [\#760](https://github.com/matrix-org/matrix-js-sdk/pull/760) + * Don't fail to start up if lazy load check fails + [\#759](https://github.com/matrix-org/matrix-js-sdk/pull/759) + * Make e2e work on Edge + [\#754](https://github.com/matrix-org/matrix-js-sdk/pull/754) + * throw error with same name and message over idb worker boundary + [\#758](https://github.com/matrix-org/matrix-js-sdk/pull/758) + * Default to a room version of 1 when there is no room create event + [\#755](https://github.com/matrix-org/matrix-js-sdk/pull/755) + * Silence bluebird warnings + [\#757](https://github.com/matrix-org/matrix-js-sdk/pull/757) + * allow non-ff merge from release branch into master + [\#750](https://github.com/matrix-org/matrix-js-sdk/pull/750) + * Reject with the actual error on indexeddb error + [\#751](https://github.com/matrix-org/matrix-js-sdk/pull/751) + * Update mocha to v5 + [\#744](https://github.com/matrix-org/matrix-js-sdk/pull/744) + * disable lazy loading for guests as they cant create filters + [\#748](https://github.com/matrix-org/matrix-js-sdk/pull/748) + * Revert "Add getMediaLimits to client" + [\#745](https://github.com/matrix-org/matrix-js-sdk/pull/745) + +Changes in [0.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1) (2018-10-01) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1-rc.1...v0.11.1) + + * No changes since rc.1 + +Changes in [0.11.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1-rc.1) (2018-09-27) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0...v0.11.1-rc.1) + + * make usage of hub compatible with latest version (2.5) + [\#747](https://github.com/matrix-org/matrix-js-sdk/pull/747) + * Detect when lazy loading has been toggled in client.startClient + [\#746](https://github.com/matrix-org/matrix-js-sdk/pull/746) + * Add getMediaLimits to client + [\#644](https://github.com/matrix-org/matrix-js-sdk/pull/644) + * Split npm start into an init and watch script + [\#742](https://github.com/matrix-org/matrix-js-sdk/pull/742) + * Revert "room name should only take canonical alias into account" + [\#738](https://github.com/matrix-org/matrix-js-sdk/pull/738) + * fix display name disambiguation with LL + [\#737](https://github.com/matrix-org/matrix-js-sdk/pull/737) + * Introduce Room.myMembership event + [\#735](https://github.com/matrix-org/matrix-js-sdk/pull/735) + * room name should only take canonical alias into account + [\#733](https://github.com/matrix-org/matrix-js-sdk/pull/733) + * state events from context response were not wrapped in a MatrixEvent + [\#732](https://github.com/matrix-org/matrix-js-sdk/pull/732) + * Reduce amount of promises created when inserting members + [\#724](https://github.com/matrix-org/matrix-js-sdk/pull/724) + * dont wait for LL members to be stored to resolve the members + [\#726](https://github.com/matrix-org/matrix-js-sdk/pull/726) + * RoomState.members emitted with wrong argument order for OOB members + [\#728](https://github.com/matrix-org/matrix-js-sdk/pull/728) + +Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (2018-09-10) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0-rc.1...v0.11.0) + +BREAKING CHANGES +---------------- + * v0.11.0-rc.1 introduced some breaking changes - see the respective release notes. + +No changes since rc.1 + +Changes in [0.11.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0-rc.1) (2018-09-07) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9...v0.11.0-rc.1) + + * Support for lazy loading members. This should improve performance for + users who joined big rooms a lot. Pass to `lazyLoadMembers = true` option when calling `startClient`. + +BREAKING CHANGES +---------------- + + * `MatrixClient::startClient` now returns a Promise. No method should be called on the client before that promise resolves. Before this method didn't return anything. + * A new `CATCHUP` sync state, emitted by `MatrixClient#"sync"` and returned by `MatrixClient::getSyncState()`, when doing initial sync after the `ERROR` state. See `MatrixClient` documentation for details. + * `RoomState::maySendEvent('m.room.message', userId)` & `RoomState::maySendMessage(userId)` do not check the membership of the user anymore, only the power level. To check if the syncing user is allowed to write in a room, use `Room::maySendMessage()` as `RoomState` is not always aware of the syncing user's membership anymore, in case lazy loading of members is enabled. + +All Changes +----------- + + * Only emit CATCHUP if recovering from conn error + [\#727](https://github.com/matrix-org/matrix-js-sdk/pull/727) + * Fix docstring for sync data.error + [\#725](https://github.com/matrix-org/matrix-js-sdk/pull/725) + * Re-apply "Don't rely on members to query if syncing user can post to room" + [\#723](https://github.com/matrix-org/matrix-js-sdk/pull/723) + * Revert "Don't rely on members to query if syncing user can post to room" + [\#721](https://github.com/matrix-org/matrix-js-sdk/pull/721) + * Don't rely on members to query if syncing user can post to room + [\#717](https://github.com/matrix-org/matrix-js-sdk/pull/717) + * Fixes for room.guessDMUserId + [\#719](https://github.com/matrix-org/matrix-js-sdk/pull/719) + * Fix filepanel also filtering main timeline with LL turned on. + [\#716](https://github.com/matrix-org/matrix-js-sdk/pull/716) + * Remove lazy loaded members when leaving room + [\#711](https://github.com/matrix-org/matrix-js-sdk/pull/711) + * Fix: show spinner again while recovering from connection error + [\#702](https://github.com/matrix-org/matrix-js-sdk/pull/702) + * Add method to query LL state in client + [\#714](https://github.com/matrix-org/matrix-js-sdk/pull/714) + * Fix: also load invited members when lazy loading members + [\#707](https://github.com/matrix-org/matrix-js-sdk/pull/707) + * Pass through function to discard megolm session + [\#704](https://github.com/matrix-org/matrix-js-sdk/pull/704) + +Changes in [0.10.9](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9) (2018-09-03) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9-rc.2...v0.10.9) + + * No changes since rc.2 + +Changes in [0.10.9-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9-rc.2) (2018-08-31) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9-rc.1...v0.10.9-rc.2) + + * Fix for "otherMember.getAvatarUrl is not a function" + [\#708](https://github.com/matrix-org/matrix-js-sdk/pull/708) + +Changes in [0.10.9-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9-rc.1) (2018-08-30) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.8...v0.10.9-rc.1) + + * Fix DM avatar + [\#706](https://github.com/matrix-org/matrix-js-sdk/pull/706) + * Lazy loading: avoid loading members at initial sync for e2e rooms + [\#699](https://github.com/matrix-org/matrix-js-sdk/pull/699) + * Improve setRoomEncryption guard against multiple m.room.encryption st… + [\#700](https://github.com/matrix-org/matrix-js-sdk/pull/700) + * Revert "Lazy loading: don't block on setting up room crypto" + [\#698](https://github.com/matrix-org/matrix-js-sdk/pull/698) + * Lazy loading: don't block on setting up room crypto + [\#696](https://github.com/matrix-org/matrix-js-sdk/pull/696) + * Add getVisibleRooms() + [\#695](https://github.com/matrix-org/matrix-js-sdk/pull/695) + * Add wrapper around getJoinedMemberCount() + [\#697](https://github.com/matrix-org/matrix-js-sdk/pull/697) + * Api to fetch events via /room/.../event/.. + [\#694](https://github.com/matrix-org/matrix-js-sdk/pull/694) + * Support for room upgrades + [\#693](https://github.com/matrix-org/matrix-js-sdk/pull/693) + * Lazy loading of room members + [\#691](https://github.com/matrix-org/matrix-js-sdk/pull/691) + +Changes in [0.10.8](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.8) (2018-08-20) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.8-rc.1...v0.10.8) + + * No changes since rc.1 + +Changes in [0.10.8-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.8-rc.1) (2018-08-16) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.7...v0.10.8-rc.1) + + * Add getVersion to Room + [\#689](https://github.com/matrix-org/matrix-js-sdk/pull/689) + * Add getSyncStateData() + [\#680](https://github.com/matrix-org/matrix-js-sdk/pull/680) + * Send sync error to listener + [\#679](https://github.com/matrix-org/matrix-js-sdk/pull/679) + * make sure room.tags is always a valid object to avoid crashes + [\#675](https://github.com/matrix-org/matrix-js-sdk/pull/675) + * Fix infinite spinner upon joining a room + [\#673](https://github.com/matrix-org/matrix-js-sdk/pull/673) + +Changes in [0.10.7](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.7) (2018-07-30) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.7-rc.1...v0.10.7) + + * No changes since rc.1 + +Changes in [0.10.7-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.7-rc.1) (2018-07-24) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6...v0.10.7-rc.1) + + * encrypt for invited users if history visibility allows. + [\#666](https://github.com/matrix-org/matrix-js-sdk/pull/666) + Changes in [0.10.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.6) (2018-07-09) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6-rc.1...v0.10.6) diff --git a/README.md b/README.md index ec11da0a3..be2fa0f86 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,61 @@ In Node.js console.log("Public Rooms: %s", JSON.stringify(data)); }); ``` + See below for how to include libolm to enable end-to-end-encryption. Please check [the Node.js terminal app](examples/node) for a more complex example. +To start the client: + +```javascript +await client.startClient({initialSyncLimit: 10}); +``` + +You can perform a call to `/sync` to get the current state of the client: + +```javascript +client.once('sync', function(state, prevState, res) { + if(state === 'PREPARED') { + console.log("prepared"); + } else { + console.log(state); + process.exit(1); + } +}); +``` + +To send a message: + +```javascript +var content = { + "body": "message text", + "msgtype": "m.text" +}; +client.sendEvent("roomId", "m.room.message", content, "", (err, res) => { + console.log(err); +}); +``` + +To listen for message events: + +```javascript +client.on("Room.timeline", function(event, room, toStartOfTimeline) { + if (event.getType() !== "m.room.message") { + return; // only use messages + } + console.log(event.event.content.body); +}); +``` + +By default, the `matrix-js-sdk` client uses the `MatrixInMemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room: + +```javascript +Object.keys(client.store.rooms).forEach((roomId) => { + client.getRoom(roomId).timeline.forEach(t => { + console.log(t.event); + }); +}); +``` What does this SDK do? ---------------------- @@ -267,13 +319,13 @@ To provide the Olm library in a browser application: To provide the Olm library in a node.js application: - * ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz`` + * ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz`` (replace the URL with the latest version you want to use from https://matrix.org/packages/npm/olm/) * ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``. If you want to package Olm as dependency for your node.js application, you -can use ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz +can use ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz --save-optional`` (if your application also works without e2e crypto enabled) or ``--save`` (if it doesn't) to do so. diff --git a/browser-index.js b/browser-index.js index 66a6036f2..cc7e50fe8 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,5 +1,17 @@ var matrixcs = require("./lib/matrix"); -matrixcs.request(require("browser-request")); +const request = require('browser-request'); +const queryString = require('qs'); + +matrixcs.request(function(opts, fn) { + // We manually fix the query string for browser-request because + // it doesn't correctly handle cases like ?via=one&via=two. Instead + // we mimic `request`'s query string interface to make it all work + // as expected. + // browser-request will happily take the constructed string as the + // query string without trying to modify it further. + opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions); + return request(opts, fn); +}); // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. diff --git a/examples/node/app.js b/examples/node/app.js index dcff22aac..fd486830b 100644 --- a/examples/node/app.js +++ b/examples/node/app.js @@ -202,9 +202,9 @@ function printRoomList() { dateStr = new Date(msg.getTs()).toISOString().replace( /T/, ' ').replace(/\..+/, ''); } - var me = roomList[i].getMember(myUserId); - if (me) { - fmt = fmts[me.membership]; + var myMembership = roomList[i].getMyMembership(); + if (myMembership) { + fmt = fmts[myMembership]; } var roomName = fixWidth(roomList[i].name, 25); print( diff --git a/jenkins.sh b/jenkins.sh index 047497bb5..87a1137b3 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -5,7 +5,7 @@ set -x export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" -nvm use 6 || exit $? +nvm use 10 || exit $? npm install || exit $? RC=0 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..df95672aa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7124 @@ +{ + "name": "matrix-js-sdk", + "version": "0.13.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz", + "integrity": "sha512-cuAuTTIQ9RqcFRJ/Y8PvTh+paepNcaGxwQwjIDRWPXmzzyAeCO4KqS9ikMvq0MCbRk6GlYKwfzStrcP3/jSL8g==", + "dev": true, + "requires": { + "@babel/highlight": "7.0.0-beta.44" + } + }, + "@babel/generator": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.44.tgz", + "integrity": "sha512-5xVb7hlhjGcdkKpMXgicAVgx8syK5VJz193k0i/0sLP6DzE6lRrU1K3B/rFefgdo9LPGMAOOOAWW4jycj07ShQ==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.44", + "jsesc": "^2.5.1", + "lodash": "^4.2.0", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz", + "integrity": "sha512-MHRG2qZMKMFaBavX0LWpfZ2e+hLloT++N7rfM3DYOMUOGCD8cVjqZpwiL8a0bOX3IYcQev1ruciT0gdFFRTxzg==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "7.0.0-beta.44", + "@babel/template": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz", + "integrity": "sha512-w0YjWVwrM2HwP6/H3sEgrSQdkCaxppqFeJtAnB23pRiJB5E/O9Yp7JAAeWBl+gGEgmBFinnTyOv2RN7rcSmMiw==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.44" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz", + "integrity": "sha512-aQ7QowtkgKKzPGf0j6u77kBMdUFVBKNHw2p/3HX/POt5/oz8ec5cs0GwlgM8Hz7ui5EwJnzyfRmkNF1Nx1N7aA==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.44" + } + }, + "@babel/highlight": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.44.tgz", + "integrity": "sha512-Il19yJvy7vMFm8AVAh6OZzaFoAd0hbkeMZiX3P5HGD+z7dyI7RzndHB0dg6Urh/VAFfHtpOIzDUSxmY6coyZWQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/template": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.44.tgz", + "integrity": "sha512-w750Sloq0UNifLx1rUqwfbnC6uSUk0mfwwgGRfdLiaUzfAOiH0tHJE6ILQIUi3KYkjiCDTskoIsnfqZvWLBDng==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "lodash": "^4.2.0" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", + "integrity": "sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==", + "dev": true + } + } + }, + "@babel/traverse": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.44.tgz", + "integrity": "sha512-UHuDz8ukQkJCDASKHf+oDt3FVUzFd+QYfuBIsiNu/4+/ix6pP/C+uQZJ6K1oEfbCMv/IKWbgDEh7fcsnIE5AtA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/generator": "7.0.0-beta.44", + "@babel/helper-function-name": "7.0.0-beta.44", + "@babel/helper-split-export-declaration": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "debug": "^3.1.0", + "globals": "^11.1.0", + "invariant": "^2.2.0", + "lodash": "^4.2.0" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", + "integrity": "sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==", + "dev": true + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "globals": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", + "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.44.tgz", + "integrity": "sha512-5eTV4WRmqbaFM3v9gHAIljEQJU4Ssc6fxL61JN+Oe2ga/BwyjzjamwkCVVAQjHGuAX8i0BWo42dshL8eO5KfLQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.2.0", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } + } + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "accessory": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/accessory/-/accessory-1.1.0.tgz", + "integrity": "sha1-eDPpg5oy3tdtJgIfNqQXB6Ug9ZM=", + "dev": true, + "requires": { + "ap": "~0.2.0", + "balanced-match": "~0.2.0", + "dot-parts": "~1.0.0" + }, + "dependencies": { + "balanced-match": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.2.1.tgz", + "integrity": "sha1-e8ZYtL7WHu5CStdPdfXD4sTfPMc=", + "dev": true + } + } + }, + "acorn": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", + "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "acorn-node": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.6.2.tgz", + "integrity": "sha512-rIhNEZuNI8ibQcL7ANm/mGyPukIaZsRNX9psFNQURyJW0nu6k8wjSDld20z6v2mDBWqX13pIEnk9gGZJHIlEXg==", + "dev": true, + "requires": { + "acorn": "^6.0.2", + "acorn-dynamic-import": "^4.0.0", + "acorn-walk": "^6.1.0", + "xtend": "^4.0.1" + } + }, + "acorn-walk": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", + "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", + "dev": true + }, + "ajv": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", + "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "optional": true + }, + "another-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", + "integrity": "sha1-tfQBnJc7bdXGUGotk0acttMq7tw=" + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "ap": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ap/-/ap-0.2.0.tgz", + "integrity": "sha1-rglCYAspkS8NKxTsYMRejzMLYRA=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "babel-cli": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz", + "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=", + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "babel-polyfill": "^6.26.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "chokidar": "^1.6.1", + "commander": "^2.11.0", + "convert-source-map": "^1.5.0", + "fs-readdir-recursive": "^1.0.0", + "glob": "^7.1.2", + "lodash": "^4.17.4", + "output-file-sync": "^1.1.2", + "path-is-absolute": "^1.0.1", + "slash": "^1.0.0", + "source-map": "^0.5.6", + "v8flags": "^2.1.1" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + } + }, + "babel-eslint": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.2.6.tgz", + "integrity": "sha512-aCdHjhzcILdP8c9lej7hvXKvQieyRt20SF102SIGyY4cUIiw6UaAtK4j2o3dXX74jEmy0TJ0CEhv4fTIM3SzcA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/traverse": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", + "integrity": "sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-transform-async-to-bluebird": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-bluebird/-/babel-plugin-transform-async-to-bluebird-1.1.1.tgz", + "integrity": "sha1-Ruo+fFr2KXgqyfHtG3zTj4Qlr9Q=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.8.0", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-template": "^6.9.0", + "babel-traverse": "^6.10.4" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true, + "requires": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true, + "requires": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true, + "requires": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "dev": true, + "requires": { + "regenerator-transform": "^0.10.0" + } + }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + } + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.24.1", + "babel-plugin-transform-es2015-classes": "^6.24.1", + "babel-plugin-transform-es2015-computed-properties": "^6.24.1", + "babel-plugin-transform-es2015-destructuring": "^6.22.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", + "babel-plugin-transform-es2015-for-of": "^6.22.0", + "babel-plugin-transform-es2015-function-name": "^6.24.1", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-umd": "^6.24.1", + "babel-plugin-transform-es2015-object-super": "^6.24.1", + "babel-plugin-transform-es2015-parameters": "^6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", + "babel-plugin-transform-regenerator": "^6.24.1" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "base-x": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.4.tgz", + "integrity": "sha512-UYOadoSIkEI/VrRGSG6qp93rp2WdokiAiNYDfGW5qURAY8GiAQkvMbwNNSDYiVJopqv4gCna7xqf4rrNGp+5AA==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "binary-extensions": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz", + "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==", + "dev": true + }, + "bluebird": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-pack": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", + "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "combine-source-map": "~0.8.0", + "defined": "^1.0.0", + "safe-buffer": "^5.1.1", + "through2": "^2.0.0", + "umd": "^3.0.0" + } + }, + "browser-request": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", + "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=" + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.2.3.tgz", + "integrity": "sha512-zQt/Gd1+W+IY+h/xX2NYMW4orQWhqSwyV+xsblycTtpOuB27h1fZhhNQuipJ4t79ohw4P4mMem0jp/ZkISQtjQ==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^1.11.0", + "browserify-zlib": "~0.2.0", + "buffer": "^5.0.2", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.0", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^2.0.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.0.0", + "labeled-stream-splicer": "^2.0.0", + "mkdirp": "^0.5.0", + "module-deps": "^6.0.0", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "~0.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^2.0.0", + "stream-http": "^2.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.10.1", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-shim": { + "version": "3.8.14", + "resolved": "https://registry.npmjs.org/browserify-shim/-/browserify-shim-3.8.14.tgz", + "integrity": "sha1-vxBXAmky0yU8de991xTzuHft7Gs=", + "dev": true, + "requires": { + "exposify": "~0.5.0", + "mothership": "~0.2.0", + "rename-function-calls": "~0.1.0", + "resolve": "~0.6.1", + "through": "~2.3.4" + }, + "dependencies": { + "resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=", + "dev": true + } + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "requires": { + "base-x": "^3.0.2" + } + }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "cached-path-relative": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", + "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "catharsis": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz", + "integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=", + "dev": true, + "requires": { + "underscore-contrib": "~0.3.0" + } + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, + "requires": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + }, + "dependencies": { + "convert-source-map": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + } + } + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~2.0.0", + "typedarray": "~0.0.5" + }, + "dependencies": { + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "d": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "^0.10.9" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + }, + "dependencies": { + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + } + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "^6.1.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "p-map": "^1.1.1", + "pify": "^3.0.0", + "rimraf": "^2.2.8" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "deps-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "shasum": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^2.0.0" + } + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "detective": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", + "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", + "dev": true, + "requires": { + "acorn": "^5.2.1", + "defined": "^1.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + } + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "dot-parts": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dot-parts/-/dot-parts-1.0.1.tgz", + "integrity": "sha1-iEvXvPwwgv+tL+XbU+SU2PPgdD8=", + "dev": true + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "elliptic": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es5-ext": { + "version": "0.10.46", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", + "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.1.0.tgz", + "integrity": "sha1-xmOSP24gqtSNDA+knzHG1PSTYM8=", + "dev": true, + "requires": { + "esprima": "~1.0.4", + "estraverse": "~1.5.0", + "esutils": "~1.0.0", + "source-map": "~0.1.30" + }, + "dependencies": { + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "eslint": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "requires": { + "babel-code-frame": "^6.16.0", + "chalk": "^1.1.3", + "concat-stream": "^1.5.2", + "debug": "^2.1.1", + "doctrine": "^2.0.0", + "escope": "^3.6.0", + "espree": "^3.4.0", + "esquery": "^1.0.0", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "glob": "^7.0.3", + "globals": "^9.14.0", + "ignore": "^3.2.0", + "imurmurhash": "^0.1.4", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "levn": "^0.3.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "shelljs": "^0.7.5", + "strip-bom": "^3.0.0", + "strip-json-comments": "~2.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.0.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + } + } + }, + "eslint-config-google": { + "version": "0.7.1", + "resolved": "http://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.7.1.tgz", + "integrity": "sha1-VZj4SY6eB4Qg80uASVuNlZ9lH7I=", + "dev": true + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "3.5.4", + "resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + } + } + }, + "esprima-fb": { + "version": "3001.1.0-dev-harmony-fb", + "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz", + "integrity": "sha1-t303q8046gt3Qmu4vCkizmtCZBE=", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "exorcist": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/exorcist/-/exorcist-0.4.0.tgz", + "integrity": "sha1-EjD/3t2SSPQvvM+LSkTUyrKePGQ=", + "dev": true, + "requires": { + "minimist": "0.0.5", + "mold-source-map": "~0.4.0", + "nave": "~0.5.1" + }, + "dependencies": { + "minimist": { + "version": "0.0.5", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", + "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=", + "dev": true + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "^2.1.0" + } + }, + "expect": { + "version": "1.20.2", + "resolved": "http://registry.npmjs.org/expect/-/expect-1.20.2.tgz", + "integrity": "sha1-1Fj+TFYAQDa64yMkFqP2Nh8E+WU=", + "dev": true, + "requires": { + "define-properties": "~1.1.2", + "has": "^1.0.1", + "is-equal": "^1.5.1", + "is-regex": "^1.0.3", + "object-inspect": "^1.1.0", + "object-keys": "^1.0.9", + "tmatch": "^2.0.1" + }, + "dependencies": { + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + } + } + }, + "exposify": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/exposify/-/exposify-0.5.0.tgz", + "integrity": "sha1-+S0AlMJls/VT4fpFagOhiD0QWcw=", + "dev": true, + "requires": { + "globo": "~1.1.0", + "map-obj": "~1.0.1", + "replace-requires": "~1.0.3", + "through2": "~0.4.0", + "transformify": "~0.1.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.4.2", + "resolved": "http://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + } + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "find-parent-dir": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", + "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", + "dev": true + }, + "flat-cache": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.2.tgz", + "integrity": "sha512-KByBY8c98sLUAGpnmjEdWTrtrLZRtZdwds+kAL/ciFXTCb7AZgqKsAnVnYFQj1hxepwO8JKN/8AsRWwLq+RK0A==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "del": "^3.0.0", + "graceful-fs": "^4.1.2", + "write": "^0.2.1" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "minipass": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", + "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", + "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", + "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", + "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", + "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz", + "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "dev": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "globo": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/globo/-/globo-1.1.0.tgz", + "integrity": "sha1-DSYJiVXepCLrIAGxBImLChAcqvM=", + "dev": true, + "requires": { + "accessory": "~1.1.0", + "is-defined": "~1.0.0", + "ternary": "~1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "handlebars": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", + "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", + "dev": true, + "requires": { + "async": "^2.5.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "uglify-js": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", + "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.17.1", + "source-map": "~0.6.1" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "has-require": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/has-require/-/has-require-1.2.2.tgz", + "integrity": "sha1-khZ1qxMNvZdo/I2o8ajiQt+kF3Q=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.3" + } + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", + "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "htmlescape": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, + "requires": { + "source-map": "~0.5.3" + } + }, + "inquirer": { + "version": "0.12.0", + "resolved": "http://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "insert-module-globals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz", + "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "acorn-node": "^1.5.2", + "combine-source-map": "^0.8.0", + "concat-stream": "^1.6.1", + "is-buffer": "^1.1.0", + "path-is-absolute": "^1.0.1", + "process": "~0.11.0", + "through2": "^2.0.0", + "undeclared-identifiers": "^1.1.2", + "xtend": "^4.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + } + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arrow-function": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-arrow-function/-/is-arrow-function-2.0.3.tgz", + "integrity": "sha1-Kb4sLY2UUIUri7r7Y1unuNjofsI=", + "dev": true, + "requires": { + "is-callable": "^1.0.4" + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-boolean-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", + "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-defined/-/is-defined-1.0.0.tgz", + "integrity": "sha1-HwfKZ9Vx9ZTEsUQVpF9774j5K/U=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/is-equal/-/is-equal-1.5.5.tgz", + "integrity": "sha1-XoXxlX4FKIMkf+s4aWWju6Ffuz0=", + "dev": true, + "requires": { + "has": "^1.0.1", + "is-arrow-function": "^2.0.3", + "is-boolean-object": "^1.0.0", + "is-callable": "^1.1.3", + "is-date-object": "^1.0.1", + "is-generator-function": "^1.0.6", + "is-number-object": "^1.0.3", + "is-regex": "^1.0.3", + "is-string": "^1.0.4", + "is-symbol": "^1.0.1", + "object.entries": "^1.0.4" + } + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz", + "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-number-object": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", + "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", + "dev": true + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-string": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", + "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, + "js2xmlparser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz", + "integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=", + "dev": true, + "requires": { + "xmlcreate": "^1.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jsdoc": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz", + "integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==", + "dev": true, + "requires": { + "babylon": "7.0.0-beta.19", + "bluebird": "~3.5.0", + "catharsis": "~0.8.9", + "escape-string-regexp": "~1.0.5", + "js2xmlparser": "~3.0.0", + "klaw": "~2.0.0", + "marked": "~0.3.6", + "mkdirp": "~0.5.1", + "requizzle": "~0.2.1", + "strip-json-comments": "~2.0.1", + "taffydb": "2.6.2", + "underscore": "~1.8.3" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.19", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz", + "integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==", + "dev": true + } + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json5": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "klaw": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz", + "integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "labeled-stream-splicer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.1.tgz", + "integrity": "sha512-MC94mHZRvJ3LfykJlTUipBqenZz1pacOZEMhhQ8dMGcDHs0SBE5GbsavUXV7YtP3icBW17W0Zy1I0lfASmo9Pg==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "isarray": "^2.0.4", + "stream-splicer": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.4.tgz", + "integrity": "sha512-GMxXOiUirWg1xTKRipM0Ek07rX+ubx4nNVElTJdNLYmNO/2YrDkgJGw9CljXn+r4EWiDQg/8lsRdHyg2PJuUaA==", + "dev": true + } + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "loglevel": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz", + "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=" + }, + "lolex": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", + "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "marked": { + "version": "0.3.19", + "resolved": "http://registry.npmjs.org/marked/-/marked-0.3.19.tgz", + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", + "dev": true + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "matrix-mock-request": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/matrix-mock-request/-/matrix-mock-request-1.2.2.tgz", + "integrity": "sha512-9u86m6rOsKekNkqUUkStWXNULrY9G+9ibwolfrmgqTmgR76EGCr9fovM+JPWn4U+TbrewvHMALpPw8OxRg0ExA==", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "expect": "^1.20.2" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "requires": { + "mime-db": "~1.37.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "mocha-jenkins-reporter": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mocha-jenkins-reporter/-/mocha-jenkins-reporter-0.4.1.tgz", + "integrity": "sha512-IqnIylrkKJG0lxeoawRkhv/uiYojMEw3o9TQOpDFarPYKVq4ymngVPwsyfMB0XMDqtDbOTOCviFg8xOLHb80/Q==", + "dev": true, + "requires": { + "diff": "1.0.7", + "mkdirp": "0.5.1", + "mocha": "^5.2.0", + "xml": "^1.0.1" + }, + "dependencies": { + "diff": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=", + "dev": true + } + } + }, + "module-deps": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.0.tgz", + "integrity": "sha512-hKPmO06so6bL/ZvqVNVqdTVO8UAYsi3tQWlCa+z9KuWhoN4KDQtb5hcqQQv58qYiDE21wIvnttZEPiDgEbpwbA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "cached-path-relative": "^1.0.0", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.0.2", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "detective": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.1.0.tgz", + "integrity": "sha512-TFHMqfOvxlgrfVzTEkNBSh9SvSNX/HfF4OFI2QFGCyPm02EsyILqnUeb5P6q7JZ3SFNTBL5t2sePRgrN4epUWQ==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "mold-source-map": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mold-source-map/-/mold-source-map-0.4.0.tgz", + "integrity": "sha1-z2fgsxxHq5uttcnCVlGGISe7gxc=", + "dev": true, + "requires": { + "convert-source-map": "^1.1.0", + "through": "~2.2.7" + }, + "dependencies": { + "through": { + "version": "2.2.7", + "resolved": "http://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=", + "dev": true + } + } + }, + "mothership": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/mothership/-/mothership-0.2.0.tgz", + "integrity": "sha1-k9SKL7w+UOKl/I7VhvW8RMZfmpk=", + "dev": true, + "requires": { + "find-parent-dir": "~0.3.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "nan": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", + "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nave": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/nave/-/nave-0.5.3.tgz", + "integrity": "sha1-Ws7HI3WFblx2yDvSGmjXE+tfG6Q=", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-inspect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", + "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "dev": true + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.entries": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", + "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.6.1", + "function-bind": "^1.1.0", + "has": "^1.0.1" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "outpipe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", + "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=", + "dev": true, + "requires": { + "shell-quote": "^1.4.2" + } + }, + "output-file-sync": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", + "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.4", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true, + "requires": { + "path-platform": "~0.11.15" + } + }, + "parse-asn1": { + "version": "5.1.1", + "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "patch-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/patch-text/-/patch-text-1.0.2.tgz", + "integrity": "sha1-S/NuZeUXM9bpjwz2LgkDTaoDSKw=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "http://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "requires": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "rename-function-calls": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/rename-function-calls/-/rename-function-calls-0.1.1.tgz", + "integrity": "sha1-f4M2nAB6MAf2q+MDPM+BaGoQjgE=", + "dev": true, + "requires": { + "detective": "~3.1.0" + }, + "dependencies": { + "detective": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-3.1.0.tgz", + "integrity": "sha1-d3gkRKt1K4jKG+Lp0KA5Xx2iXu0=", + "dev": true, + "requires": { + "escodegen": "~1.1.0", + "esprima-fb": "3001.1.0-dev-harmony-fb" + } + } + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "replace-requires": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/replace-requires/-/replace-requires-1.0.4.tgz", + "integrity": "sha1-AUtzMLa54lV7cQQ7ZvsCZgw79mc=", + "dev": true, + "requires": { + "detective": "^4.5.0", + "has-require": "~1.2.1", + "patch-text": "~1.0.2", + "xtend": "~4.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "requizzle": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz", + "integrity": "sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=", + "dev": true, + "requires": { + "underscore": "~1.6.0" + }, + "dependencies": { + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", + "dev": true + } + } + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "^1.3.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "sha.js": { + "version": "2.4.11", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shasum": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true, + "requires": { + "json-stable-stringify": "~0.0.0", + "sha.js": "~2.4.4" + } + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "http://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sourceify": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sourceify/-/sourceify-0.1.0.tgz", + "integrity": "sha1-C1b+V/lFc1DZJliBCq/afA9EA0w=", + "dev": true, + "requires": { + "convert-source-map": "^1.1.3", + "through2": "^2.0.0" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "requires": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-splicer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", + "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "requires": { + "minimist": "^1.1.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, + "requires": { + "acorn-node": "^1.2.0" + } + }, + "table": { + "version": "3.8.3", + "resolved": "http://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", + "slice-ansi": "0.0.4", + "string-width": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "taffydb": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", + "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", + "dev": true + }, + "ternary": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ternary/-/ternary-1.0.0.tgz", + "integrity": "sha1-RXAnJWCMlJnUapYQ6bDkn/JveJ4=", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, + "requires": { + "process": "~0.11.0" + } + }, + "tmatch": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-2.0.1.tgz", + "integrity": "sha1-DFYkbzPzDaG409colauvFmYPOM8=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "transformify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/transformify/-/transformify-0.1.2.tgz", + "integrity": "sha1-mk9CoVRDPdcnuAV1Qoo8nlSJ6/E=", + "dev": true, + "requires": { + "readable-stream": "~1.1.9" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", + "dev": true + }, + "undeclared-identifiers": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.2.tgz", + "integrity": "sha512-13EaeocO4edF/3JKime9rD7oB6QI8llAGhgn5fKOPyfkJbRb6NFv9pYV6dFEmpa4uRjKeBqLZP8GpuzqHlKDMQ==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "get-assigned-identifiers": "^1.2.0", + "simple-concat": "^1.0.0", + "xtend": "^4.0.1" + } + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", + "dev": true + }, + "underscore-contrib": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/underscore-contrib/-/underscore-contrib-0.3.0.tgz", + "integrity": "sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc=", + "dev": true, + "requires": { + "underscore": "1.6.0" + }, + "dependencies": { + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", + "dev": true + } + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true, + "requires": { + "user-home": "^1.1.1" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, + "watchify": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.11.0.tgz", + "integrity": "sha512-7jWG0c3cKKm2hKScnSAMUEUjRJKXUShwMPk0ASVhICycQhwND3IMAdhJYmc1mxxKzBUJTSF5HZizfrKrS6BzkA==", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "browserify": "^16.1.0", + "chokidar": "^1.0.0", + "defined": "^1.0.0", + "outpipe": "^1.1.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "browserify": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.2.3.tgz", + "integrity": "sha512-zQt/Gd1+W+IY+h/xX2NYMW4orQWhqSwyV+xsblycTtpOuB27h1fZhhNQuipJ4t79ohw4P4mMem0jp/ZkISQtjQ==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^1.11.0", + "browserify-zlib": "~0.2.0", + "buffer": "^5.0.2", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.0", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^2.0.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.0.0", + "labeled-stream-splicer": "^2.0.0", + "mkdirp": "^0.5.0", + "module-deps": "^6.0.0", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "~0.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^2.0.0", + "stream-http": "^2.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.10.1", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "detective": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.1.0.tgz", + "integrity": "sha512-TFHMqfOvxlgrfVzTEkNBSh9SvSNX/HfF4OFI2QFGCyPm02EsyILqnUeb5P6q7JZ3SFNTBL5t2sePRgrN4epUWQ==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "module-deps": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.0.tgz", + "integrity": "sha512-hKPmO06so6bL/ZvqVNVqdTVO8UAYsi3tQWlCa+z9KuWhoN4KDQtb5hcqQQv58qYiDE21wIvnttZEPiDgEbpwbA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "cached-path-relative": "^1.0.0", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.0.2", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "xmlcreate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", + "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/package.json b/package.json index 63809fa60..8adb86bf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.6", + "version": "0.14.2", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { @@ -10,7 +10,9 @@ "test": "npm run test:build && npm run test:run", "check": "npm run test:build && _mocha --recursive specbuild --colors", "gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc", - "start": "babel -s -w -d lib src", + "start": "npm run start:init && npm run start:watch", + "start:watch": "babel -s -w --skip-initial-build -d lib src", + "start:init": "babel -s -d lib src", "clean": "rimraf lib dist", "build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js", "dist": "npm run build", @@ -19,6 +21,7 @@ "prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt" }, "repository": { + "type": "git", "url": "https://github.com/matrix-org/matrix-js-sdk" }, "keywords": [ @@ -53,17 +56,20 @@ "babel-runtime": "^6.26.0", "bluebird": "^3.5.0", "browser-request": "^0.3.3", + "bs58": "^4.0.1", "content-type": "^1.0.2", - "request": "^2.53.0", + "loglevel": "1.6.1", + "qs": "^6.5.2", + "request": "^2.88.0", "unhomoglyph": "^1.0.2" }, "devDependencies": { "babel-cli": "^6.18.0", - "babel-eslint": "^7.1.1", + "babel-eslint": "^8.1.1", "babel-plugin-transform-async-to-bluebird": "^1.1.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-es2015": "^6.18.0", - "browserify": "^14.0.0", + "browserify": "^16.2.3", "browserify-shim": "^3.8.13", "eslint": "^3.13.1", "eslint-config-google": "^0.7.1", @@ -72,14 +78,14 @@ "istanbul": "^0.4.5", "jsdoc": "^3.5.5", "lolex": "^1.5.2", - "matrix-mock-request": "^1.2.0", - "mocha": "^3.2.0", - "mocha-jenkins-reporter": "^0.3.6", + "matrix-mock-request": "^1.2.2", + "mocha": "^5.2.0", + "mocha-jenkins-reporter": "^0.4.0", "rimraf": "^2.5.4", "source-map-support": "^0.4.11", "sourceify": "^0.1.0", "uglify-js": "^2.8.26", - "watchify": "^3.2.1" + "watchify": "^3.11.0" }, "browserify": { "transform": [ diff --git a/release.sh b/release.sh index 5a0538a58..c2d8a1344 100755 --- a/release.sh +++ b/release.sh @@ -11,7 +11,17 @@ set -e jq --version > /dev/null || (echo "jq is required: please install it"; kill $$) -hub --version > /dev/null || (echo "hub is required: please install it"; kill $$) +if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then + HUB_VERSION_MAJOR=${BASH_REMATCH[1]} + HUB_VERSION_MINOR=${BASH_REMATCH[2]} + if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then + echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed" + exit + fi +else + echo "hub is required: please install it" + exit +fi USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z" @@ -45,7 +55,8 @@ fi skip_changelog= skip_jsdoc= changelog_file="CHANGELOG.md" -while getopts hc:xz f; do +expected_npm_user="matrixdotorg" +while getopts hc:u:xz f; do case $f in h) help @@ -60,6 +71,9 @@ while getopts hc:xz f; do z) skip_jsdoc=1 ;; + u) + expected_npm_user="$OPTARG" + ;; esac done shift `expr $OPTIND - 1` @@ -74,6 +88,12 @@ if [ -z "$skip_changelog" ]; then update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit) fi +actual_npm_user=`npm whoami`; +if [ $expected_npm_user != $actual_npm_user ]; then + echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2 + exit 1 +fi + # ignore leading v on release release="${1#v}" tag="v${release}" @@ -245,7 +265,7 @@ release_text=`mktemp` echo "$tag" > "${release_text}" echo >> "${release_text}" cat "${latest_changes}" >> "${release_text}" -hub release create $hubflags $assets -f "${release_text}" "$tag" +hub release create $hubflags $assets -F "${release_text}" "$tag" if [ $dodist -eq 0 ]; then rm -rf "$builddir" @@ -281,7 +301,7 @@ fi echo "updating master branch" git checkout master git pull -git merge --ff-only "$rel_branch" +git merge "$rel_branch" # push master and docs (if generated) to github git push origin master diff --git a/spec/TestClient.js b/spec/TestClient.js index 1a7fa0816..1c08c2c7a 100644 --- a/spec/TestClient.js +++ b/spec/TestClient.js @@ -102,9 +102,11 @@ TestClient.prototype.start = function() { /** * stop the client + * @return {Promise} Resolves once the mock http backend has finished all pending flushes */ TestClient.prototype.stop = function() { this.client.stopClient(); + return this.httpBackend.stop(); }; /** diff --git a/spec/integ/devicelist-integ-spec.js b/spec/integ/devicelist-integ-spec.js index f3f889deb..b671ce5bf 100644 --- a/spec/integ/devicelist-integ-spec.js +++ b/spec/integ/devicelist-integ-spec.js @@ -97,7 +97,7 @@ describe("DeviceList management:", function() { }); afterEach(function() { - aliceTestClient.stop(); + return aliceTestClient.stop(); }); it("Alice shouldn't do a second /query for non-e2e-capable devices", function() { diff --git a/spec/integ/matrix-client-crypto.spec.js b/spec/integ/matrix-client-crypto.spec.js index 31a40aaf6..96bdd408e 100644 --- a/spec/integ/matrix-client-crypto.spec.js +++ b/spec/integ/matrix-client-crypto.spec.js @@ -410,10 +410,10 @@ describe("MatrixClient crypto", function() { }); afterEach(function() { - aliTestClient.stop(); aliTestClient.httpBackend.verifyNoOutstandingExpectation(); - bobTestClient.stop(); bobTestClient.httpBackend.verifyNoOutstandingExpectation(); + + return Promise.all([aliTestClient.stop(), bobTestClient.stop()]); }); it("Bob uploads device keys", function() { diff --git a/spec/integ/matrix-client-event-emitter.spec.js b/spec/integ/matrix-client-event-emitter.spec.js index c494e8bc4..e2fc810af 100644 --- a/spec/integ/matrix-client-event-emitter.spec.js +++ b/spec/integ/matrix-client-event-emitter.spec.js @@ -30,6 +30,7 @@ describe("MatrixClient events", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); + return httpBackend.stop(); }); describe("emissions", function() { diff --git a/spec/integ/matrix-client-event-timeline.spec.js b/spec/integ/matrix-client-event-timeline.spec.js index 6fd3778be..2f1c9fa87 100644 --- a/spec/integ/matrix-client-event-timeline.spec.js +++ b/spec/integ/matrix-client-event-timeline.spec.js @@ -111,6 +111,7 @@ describe("getEventTimeline support", function() { if (client) { client.stopClient(); } + return httpBackend.stop(); }); it("timeline support must be enabled to work", function(done) { diff --git a/spec/integ/matrix-client-methods.spec.js b/spec/integ/matrix-client-methods.spec.js index 57a63a842..e27a1a5b7 100644 --- a/spec/integ/matrix-client-methods.spec.js +++ b/spec/integ/matrix-client-methods.spec.js @@ -41,6 +41,7 @@ describe("MatrixClient", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); + return httpBackend.stop(); }); describe("uploadContent", function() { @@ -159,7 +160,7 @@ describe("MatrixClient", function() { describe("joinRoom", function() { it("should no-op if you've already joined a room", function() { const roomId = "!foo:bar"; - const room = new Room(roomId); + const room = new Room(roomId, userId); room.addLiveEvents([ utils.mkMembership({ user: userId, room: roomId, mship: "join", event: true, diff --git a/spec/integ/matrix-client-opts.spec.js b/spec/integ/matrix-client-opts.spec.js index c7388678a..f2bc17e01 100644 --- a/spec/integ/matrix-client-opts.spec.js +++ b/spec/integ/matrix-client-opts.spec.js @@ -64,6 +64,7 @@ describe("MatrixClient opts", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); + return httpBackend.stop(); }); describe("without opts.store", function() { @@ -94,7 +95,7 @@ describe("MatrixClient opts", function() { httpBackend.flush("/txn1", 1); }); - it("should be able to sync / get new events", function(done) { + it("should be able to sync / get new events", async function() { const expectedEventTypes = [ // from /initialSync "m.room.message", "m.room.name", "m.room.member", "m.room.member", "m.room.create", @@ -110,20 +111,16 @@ describe("MatrixClient opts", function() { httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" }); httpBackend.when("GET", "/sync").respond(200, syncData); - client.startClient(); - httpBackend.flush("/pushrules", 1).then(function() { - return httpBackend.flush("/filter", 1); - }).then(function() { - return Promise.all([ - httpBackend.flush("/sync", 1), - utils.syncPromise(client), - ]); - }).done(function() { - expect(expectedEventTypes.length).toEqual( - 0, "Expected to see event types: " + expectedEventTypes, - ); - done(); - }); + await client.startClient(); + await httpBackend.flush("/pushrules", 1); + await httpBackend.flush("/filter", 1); + await Promise.all([ + httpBackend.flush("/sync", 1), + utils.syncPromise(client), + ]); + expect(expectedEventTypes.length).toEqual( + 0, "Expected to see event types: " + expectedEventTypes, + ); }); }); diff --git a/spec/integ/matrix-client-retrying.spec.js b/spec/integ/matrix-client-retrying.spec.js index af59e1ef6..b82294f2c 100644 --- a/spec/integ/matrix-client-retrying.spec.js +++ b/spec/integ/matrix-client-retrying.spec.js @@ -36,6 +36,7 @@ describe("MatrixClient retrying", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); + return httpBackend.stop(); }); xit("should retry according to MatrixScheduler.retryFn", function() { diff --git a/spec/integ/matrix-client-room-timeline.spec.js b/spec/integ/matrix-client-room-timeline.spec.js index fdc5ea097..5970ceb66 100644 --- a/spec/integ/matrix-client-room-timeline.spec.js +++ b/spec/integ/matrix-client-room-timeline.spec.js @@ -130,6 +130,7 @@ describe("MatrixClient room timelines", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); + return httpBackend.stop(); }); describe("local echo events", function() { diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js index a74eea53a..6002436c4 100644 --- a/spec/integ/matrix-client-syncing.spec.js +++ b/spec/integ/matrix-client-syncing.spec.js @@ -38,6 +38,7 @@ describe("MatrixClient syncing", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); + return httpBackend.stop(); }); describe("startClient", function() { diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index cb59ab5da..b302856cc 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -296,7 +296,7 @@ describe("megolm", function() { }); afterEach(function() { - aliceTestClient.stop(); + return aliceTestClient.stop(); }); it("Alice receives a megolm message", function() { @@ -817,8 +817,14 @@ describe("megolm", function() { }; }); + // Grab the event that we'll need to resend + const room = aliceTestClient.client.getRoom(ROOM_ID); + const pendingEvents = room.getPendingEvents(); + expect(pendingEvents.length).toEqual(1); + const unsentEvent = pendingEvents[0]; + return Promise.all([ - aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'), + aliceTestClient.client.resendEvent(unsentEvent, room), // the crypto stuff can take a while, so give the requests a whole second. aliceTestClient.httpBackend.flushAllExpected({ diff --git a/spec/unit/autodiscovery.spec.js b/spec/unit/autodiscovery.spec.js new file mode 100644 index 000000000..9928e098b --- /dev/null +++ b/spec/unit/autodiscovery.spec.js @@ -0,0 +1,670 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; + +import 'source-map-support/register'; +import Promise from 'bluebird'; +const sdk = require("../.."); +const utils = require("../test-utils"); + +const AutoDiscovery = sdk.AutoDiscovery; + +import expect from 'expect'; +import MockHttpBackend from "matrix-mock-request"; + + +describe("AutoDiscovery", function() { + let httpBackend = null; + + beforeEach(function() { + utils.beforeEach(this); // eslint-disable-line no-invalid-this + httpBackend = new MockHttpBackend(); + sdk.request(httpBackend.requestFn); + }); + + it("should throw an error when no domain is specified", function() { + return Promise.all([ + AutoDiscovery.findClientConfig(/* no args */).then(() => { + throw new Error("Expected a failure, not success with no args"); + }, () => { + return true; + }), + + AutoDiscovery.findClientConfig("").then(() => { + throw new Error("Expected a failure, not success with an empty string"); + }, () => { + return true; + }), + + AutoDiscovery.findClientConfig(null).then(() => { + throw new Error("Expected a failure, not success with null"); + }, () => { + return true; + }), + + AutoDiscovery.findClientConfig(true).then(() => { + throw new Error("Expected a failure, not success with a non-string"); + }, () => { + return true; + }), + ]); + }); + + it("should return PROMPT when .well-known 404s", function() { + httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {}); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "PROMPT", + error: null, + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_PROMPT when .well-known returns a 500 error", function() { + httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {}); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_PROMPT", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_PROMPT when .well-known returns a 400 error", function() { + httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {}); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_PROMPT", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_PROMPT when .well-known returns an empty body", function() { + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, ""); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_PROMPT", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_PROMPT when .well-known returns not-JSON", function() { + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc"); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_PROMPT", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_PROMPT when .well-known does not have a base_url for " + + "m.homeserver (empty string)", function() { + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + base_url: "", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_PROMPT", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_PROMPT when .well-known does not have a base_url for " + + "m.homeserver (no property)", function() { + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": {}, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_PROMPT", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_ERROR when .well-known has an invalid base_url for " + + "m.homeserver (disallowed scheme)", function() { + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + base_url: "mxc://example.org", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_ERROR", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_ERROR when .well-known has an invalid base_url for " + + "m.homeserver (verification failure: 404)", function() { + httpBackend.when("GET", "/_matrix/client/versions").respond(404, {}); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + base_url: "https://example.org", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_ERROR", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_ERROR when .well-known has an invalid base_url for " + + "m.homeserver (verification failure: 500)", function() { + httpBackend.when("GET", "/_matrix/client/versions").respond(500, {}); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + base_url: "https://example.org", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_ERROR", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_ERROR when .well-known has an invalid base_url for " + + "m.homeserver (verification failure: 200 but wrong content)", function() { + httpBackend.when("GET", "/_matrix/client/versions").respond(200, { + not_matrix_versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + base_url: "https://example.org", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_ERROR", + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return SUCCESS when .well-known has a verifiably accurate base_url for " + + "m.homeserver", function() { + httpBackend.when("GET", "/_matrix/client/versions").check((req) => { + expect(req.opts.uri).toEqual("https://example.org/_matrix/client/versions"); + }).respond(200, { + versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + base_url: "https://example.org", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "SUCCESS", + error: null, + base_url: "https://example.org", + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return SUCCESS with the right homeserver URL", function() { + httpBackend.when("GET", "/_matrix/client/versions").check((req) => { + expect(req.opts.uri) + .toEqual("https://chat.example.org/_matrix/client/versions"); + }).respond(200, { + versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + // Note: we also expect this test to trim the trailing slash + base_url: "https://chat.example.org/", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "SUCCESS", + error: null, + base_url: "https://chat.example.org", + }, + "m.identity_server": { + state: "PROMPT", + error: null, + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_ERROR when the identity server configuration is wrong " + + "(missing base_url)", function() { + httpBackend.when("GET", "/_matrix/client/versions").check((req) => { + expect(req.opts.uri) + .toEqual("https://chat.example.org/_matrix/client/versions"); + }).respond(200, { + versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + // Note: we also expect this test to trim the trailing slash + base_url: "https://chat.example.org/", + }, + "m.identity_server": { + not_base_url: "https://identity.example.org", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_ERROR", + error: "Invalid identity server discovery response", + + // We still expect the base_url to be here for debugging purposes. + base_url: "https://chat.example.org", + }, + "m.identity_server": { + state: "FAIL_ERROR", + error: "Invalid identity server discovery response", + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_ERROR when the identity server configuration is wrong " + + "(empty base_url)", function() { + httpBackend.when("GET", "/_matrix/client/versions").check((req) => { + expect(req.opts.uri) + .toEqual("https://chat.example.org/_matrix/client/versions"); + }).respond(200, { + versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + // Note: we also expect this test to trim the trailing slash + base_url: "https://chat.example.org/", + }, + "m.identity_server": { + base_url: "", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_ERROR", + error: "Invalid identity server discovery response", + + // We still expect the base_url to be here for debugging purposes. + base_url: "https://chat.example.org", + }, + "m.identity_server": { + state: "FAIL_ERROR", + error: "Invalid identity server discovery response", + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_ERROR when the identity server configuration is wrong " + + "(validation error: 404)", function() { + httpBackend.when("GET", "/_matrix/client/versions").check((req) => { + expect(req.opts.uri) + .toEqual("https://chat.example.org/_matrix/client/versions"); + }).respond(200, { + versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {}); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + // Note: we also expect this test to trim the trailing slash + base_url: "https://chat.example.org/", + }, + "m.identity_server": { + base_url: "https://identity.example.org", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_ERROR", + error: "Invalid identity server discovery response", + + // We still expect the base_url to be here for debugging purposes. + base_url: "https://chat.example.org", + }, + "m.identity_server": { + state: "FAIL_ERROR", + error: "Invalid identity server discovery response", + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return FAIL_ERROR when the identity server configuration is wrong " + + "(validation error: 500)", function() { + httpBackend.when("GET", "/_matrix/client/versions").check((req) => { + expect(req.opts.uri) + .toEqual("https://chat.example.org/_matrix/client/versions"); + }).respond(200, { + versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {}); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + // Note: we also expect this test to trim the trailing slash + base_url: "https://chat.example.org/", + }, + "m.identity_server": { + base_url: "https://identity.example.org", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "FAIL_ERROR", + error: "Invalid identity server discovery response", + + // We still expect the base_url to be here for debugging purposes + base_url: "https://chat.example.org", + }, + "m.identity_server": { + state: "FAIL_ERROR", + error: "Invalid identity server discovery response", + base_url: null, + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return SUCCESS when the identity server configuration is " + + "verifiably accurate", function() { + httpBackend.when("GET", "/_matrix/client/versions").check((req) => { + expect(req.opts.uri) + .toEqual("https://chat.example.org/_matrix/client/versions"); + }).respond(200, { + versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => { + expect(req.opts.uri) + .toEqual("https://identity.example.org/_matrix/identity/api/v1"); + }).respond(200, {}); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + // Note: we also expect this test to trim the trailing slash + base_url: "https://chat.example.org/", + }, + "m.identity_server": { + base_url: "https://identity.example.org", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "SUCCESS", + error: null, + base_url: "https://chat.example.org", + }, + "m.identity_server": { + state: "SUCCESS", + error: null, + base_url: "https://identity.example.org", + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); + + it("should return SUCCESS and preserve non-standard keys from the " + + ".well-known response", function() { + httpBackend.when("GET", "/_matrix/client/versions").check((req) => { + expect(req.opts.uri) + .toEqual("https://chat.example.org/_matrix/client/versions"); + }).respond(200, { + versions: ["r0.0.1"], + }); + httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => { + expect(req.opts.uri) + .toEqual("https://identity.example.org/_matrix/identity/api/v1"); + }).respond(200, {}); + httpBackend.when("GET", "/.well-known/matrix/client").respond(200, { + "m.homeserver": { + // Note: we also expect this test to trim the trailing slash + base_url: "https://chat.example.org/", + }, + "m.identity_server": { + base_url: "https://identity.example.org", + }, + "org.example.custom.property": { + cupcakes: "yes", + }, + }); + return Promise.all([ + httpBackend.flushAllExpected(), + AutoDiscovery.findClientConfig("example.org").then((conf) => { + const expected = { + "m.homeserver": { + state: "SUCCESS", + error: null, + base_url: "https://chat.example.org", + }, + "m.identity_server": { + state: "SUCCESS", + error: null, + base_url: "https://identity.example.org", + }, + "org.example.custom.property": { + cupcakes: "yes", + }, + }; + + expect(conf).toEqual(expected); + }), + ]); + }); +}); diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 4d949e05e..522060e36 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -1,20 +1,122 @@ - -"use strict"; import 'source-map-support/register'; -const sdk = require("../.."); -let Crypto; -if (sdk.CRYPTO_ENABLED) { - Crypto = require("../../lib/crypto"); -} +import '../olm-loader'; +import Crypto from '../../lib/crypto'; import expect from 'expect'; +import WebStorageSessionStore from '../../lib/store/session/webstorage'; +import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js'; +import MockStorageApi from '../MockStorageApi'; + +const EventEmitter = require("events").EventEmitter; + +const sdk = require("../.."); + +const Olm = global.Olm; + describe("Crypto", function() { if (!sdk.CRYPTO_ENABLED) { return; } + + beforeEach(function(done) { + Olm.init().then(done); + }); + it("Crypto exposes the correct olm library version", function() { - expect(Crypto.getOlmVersion()[0]).toEqual(2); + expect(Crypto.getOlmVersion()[0]).toEqual(3); + }); + + + describe('Session management', function() { + const otkResponse = { + one_time_keys: { + '@alice:home.server': { + aliceDevice: { + 'signed_curve25519:FLIBBLE': { + key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI', + signatures: { + '@alice:home.server': { + 'ed25519:aliceDevice': 'totally a valid signature', + }, + }, + }, + }, + }, + }, + }; + let crypto; + let mockBaseApis; + let mockRoomList; + + let fakeEmitter; + + beforeEach(async function() { + const mockStorage = new MockStorageApi(); + const sessionStore = new WebStorageSessionStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(mockStorage); + + cryptoStore.storeEndToEndDeviceData({ + devices: { + '@bob:home.server': { + 'BOBDEVICE': { + keys: { + 'curve25519:BOBDEVICE': 'this is a key', + }, + }, + }, + }, + trackingStatus: {}, + }); + + mockBaseApis = { + sendToDevice: expect.createSpy(), + getKeyBackupVersion: expect.createSpy(), + isGuest: expect.createSpy(), + }; + mockRoomList = {}; + + fakeEmitter = new EventEmitter(); + + crypto = new Crypto( + mockBaseApis, + sessionStore, + "@alice:home.server", + "FLIBBLE", + sessionStore, + cryptoStore, + mockRoomList, + ); + crypto.registerEventHandlers(fakeEmitter); + await crypto.init(); + }); + + afterEach(async function() { + await crypto.stop(); + }); + + it("restarts wedged Olm sessions", async function() { + const prom = new Promise((resolve) => { + mockBaseApis.claimOneTimeKeys = function() { + resolve(); + return otkResponse; + }; + }); + + fakeEmitter.emit('toDeviceEvent', { + getType: expect.createSpy().andReturn('m.room.message'), + getContent: expect.createSpy().andReturn({ + msgtype: 'm.bad.encrypted', + }), + getWireContent: expect.createSpy().andReturn({ + algorithm: 'm.olm.v1.curve25519-aes-sha2', + sender_key: 'this is a key', + }), + getSender: expect.createSpy().andReturn('@bob:home.server'), + }); + + await prom; + }); }); }); diff --git a/spec/unit/crypto/DeviceList.spec.js b/spec/unit/crypto/DeviceList.spec.js index 870e40bbc..5a442d81b 100644 --- a/spec/unit/crypto/DeviceList.spec.js +++ b/spec/unit/crypto/DeviceList.spec.js @@ -59,16 +59,25 @@ describe('DeviceList', function() { let downloadSpy; let sessionStore; let cryptoStore; + let deviceLists = []; beforeEach(function() { testUtils.beforeEach(this); // eslint-disable-line no-invalid-this + deviceLists = []; + downloadSpy = expect.createSpy(); const mockStorage = new MockStorageApi(); sessionStore = new WebStorageSessionStore(mockStorage); cryptoStore = new MemoryCryptoStore(); }); + afterEach(function() { + for (const dl of deviceLists) { + dl.stop(); + } + }); + function createTestDeviceList() { const baseApis = { downloadKeysForUsers: downloadSpy, @@ -76,7 +85,9 @@ describe('DeviceList', function() { const mockOlm = { verifySignature: function(key, message, signature) {}, }; - return new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm); + const dl = new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm); + deviceLists.push(dl); + return dl; } it("should successfully download and store device keys", function() { diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index cf8e58f2e..164158e90 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -1,8 +1,4 @@ -try { - global.Olm = require('olm'); -} catch (e) { - console.warn("unable to run megolm tests: libolm not available"); -} +import '../../../olm-loader'; import expect from 'expect'; import Promise from 'bluebird'; @@ -13,20 +9,17 @@ import WebStorageSessionStore from '../../../../lib/store/session/webstorage'; import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js'; import MockStorageApi from '../../../MockStorageApi'; import testUtils from '../../../test-utils'; - -// Crypto and OlmDevice won't import unless we have global.Olm -let OlmDevice; -let Crypto; -if (global.Olm) { - OlmDevice = require('../../../../lib/crypto/OlmDevice'); - Crypto = require('../../../../lib/crypto'); -} +import OlmDevice from '../../../../lib/crypto/OlmDevice'; +import Crypto from '../../../../lib/crypto'; const MatrixEvent = sdk.MatrixEvent; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; +const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const ROOM_ID = '!ROOM:ID'; +const Olm = global.Olm; + describe("MegolmDecryption", function() { if (!global.Olm) { console.warn('Not running megolm unit tests: libolm not present'); @@ -38,9 +31,11 @@ describe("MegolmDecryption", function() { let mockCrypto; let mockBaseApis; - beforeEach(function() { + beforeEach(async function() { testUtils.beforeEach(this); // eslint-disable-line no-invalid-this + await Olm.init(); + mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockBaseApis = {}; @@ -69,7 +64,7 @@ describe("MegolmDecryption", function() { describe('receives some keys:', function() { let groupSession; - beforeEach(function() { + beforeEach(async function() { groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); @@ -98,7 +93,7 @@ describe("MegolmDecryption", function() { }, }; - return event.attemptDecryption(mockCrypto).then(() => { + await event.attemptDecryption(mockCrypto).then(() => { megolmDecryption.onRoomKeyEvent(event); }); }); @@ -266,5 +261,92 @@ describe("MegolmDecryption", function() { // test is successful if no exception is thrown }); }); + + it("re-uses sessions for sequential messages", async function() { + const mockStorage = new MockStorageApi(); + const sessionStore = new WebStorageSessionStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(mockStorage); + + const olmDevice = new OlmDevice(sessionStore, cryptoStore); + olmDevice.verifySignature = expect.createSpy(); + await olmDevice.init(); + + mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({ + one_time_keys: { + '@alice:home.server': { + aliceDevice: { + 'signed_curve25519:flooble': { + key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI', + signatures: { + '@alice:home.server': { + 'ed25519:aliceDevice': 'totally valid', + }, + }, + }, + }, + }, + }, + })); + mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve()); + + mockCrypto.downloadKeys.andReturn(Promise.resolve({ + '@alice:home.server': { + aliceDevice: { + deviceId: 'aliceDevice', + isBlocked: expect.createSpy().andReturn(false), + isUnverified: expect.createSpy().andReturn(false), + getIdentityKey: expect.createSpy().andReturn( + 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE', + ), + getFingerprint: expect.createSpy().andReturn(''), + }, + }, + })); + + const megolmEncryption = new MegolmEncryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: mockBaseApis, + roomId: ROOM_ID, + config: { + rotation_period_ms: 9999999999999, + }, + }); + const mockRoom = { + getEncryptionTargetMembers: expect.createSpy().andReturn( + [{userId: "@alice:home.server"}], + ), + getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false), + }; + const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", { + body: "Some text", + }); + expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled(); + + // this should have claimed a key for alice as it's starting a new session + expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled( + [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', + ); + expect(mockCrypto.downloadKeys).toHaveBeenCalledWith( + ['@alice:home.server'], false, + ); + expect(mockBaseApis.sendToDevice).toHaveBeenCalled(); + expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled( + [['@alice:home.server', 'aliceDevice']], 'signed_curve25519', + ); + + mockBaseApis.claimOneTimeKeys.reset(); + + const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", { + body: "Some more text", + }); + + // this should *not* have claimed a key as it should be using the same session + expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled(); + + // likewise they should show the same session ID + expect(ct2.session_id).toEqual(ct1.session_id); + }); }); }); diff --git a/spec/unit/crypto/algorithms/olm.spec.js b/spec/unit/crypto/algorithms/olm.spec.js new file mode 100644 index 000000000..7b6bc626a --- /dev/null +++ b/spec/unit/crypto/algorithms/olm.spec.js @@ -0,0 +1,86 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import '../../../olm-loader'; + +import expect from 'expect'; +import WebStorageSessionStore from '../../../../lib/store/session/webstorage'; +import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js'; +import MockStorageApi from '../../../MockStorageApi'; +import testUtils from '../../../test-utils'; + +import OlmDevice from '../../../../lib/crypto/OlmDevice'; + +function makeOlmDevice() { + const mockStorage = new MockStorageApi(); + const sessionStore = new WebStorageSessionStore(mockStorage); + const cryptoStore = new MemoryCryptoStore(mockStorage); + const olmDevice = new OlmDevice(sessionStore, cryptoStore); + return olmDevice; +} + +async function setupSession(initiator, opponent) { + await opponent.generateOneTimeKeys(1); + const keys = await opponent.getOneTimeKeys(); + const firstKey = Object.values(keys['curve25519'])[0]; + + const sid = await initiator.createOutboundSession( + opponent.deviceCurve25519Key, firstKey, + ); + return sid; +} + +describe("OlmDecryption", function() { + if (!global.Olm) { + console.warn('Not running megolm unit tests: libolm not present'); + return; + } + + let aliceOlmDevice; + let bobOlmDevice; + + beforeEach(async function() { + testUtils.beforeEach(this); // eslint-disable-line no-invalid-this + + await global.Olm.init(); + + aliceOlmDevice = makeOlmDevice(); + bobOlmDevice = makeOlmDevice(); + await aliceOlmDevice.init(); + await bobOlmDevice.init(); + }); + + describe('olm', function() { + it("can decrypt messages", async function() { + const sid = await setupSession(aliceOlmDevice, bobOlmDevice); + + const ciphertext = await aliceOlmDevice.encryptMessage( + bobOlmDevice.deviceCurve25519Key, + sid, + "The olm or proteus is an aquatic salamander in the family Proteidae", + ); + + const result = await bobOlmDevice.createInboundSession( + aliceOlmDevice.deviceCurve25519Key, + ciphertext.type, + ciphertext.body, + ); + expect(result.payload).toEqual( + "The olm or proteus is an aquatic salamander in the family Proteidae", + ); + }); + }); +}); diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js new file mode 100644 index 000000000..1bccb5a74 --- /dev/null +++ b/spec/unit/crypto/backup.spec.js @@ -0,0 +1,467 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import '../../olm-loader'; + +import expect from 'expect'; +import Promise from 'bluebird'; + +import sdk from '../../..'; +import algorithms from '../../../lib/crypto/algorithms'; +import WebStorageSessionStore from '../../../lib/store/session/webstorage'; +import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js'; +import MockStorageApi from '../../MockStorageApi'; +import testUtils from '../../test-utils'; + +import OlmDevice from '../../../lib/crypto/OlmDevice'; +import Crypto from '../../../lib/crypto'; + +const Olm = global.Olm; + +const MatrixClient = sdk.MatrixClient; +const MatrixEvent = sdk.MatrixEvent; +const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; + +const ROOM_ID = '!ROOM:ID'; + +const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc'; +const ENCRYPTED_EVENT = new MatrixEvent({ + type: 'm.room.encrypted', + room_id: '!ROOM:ID', + content: { + algorithm: 'm.megolm.v1.aes-sha2', + sender_key: 'SENDER_CURVE25519', + session_id: SESSION_ID, + ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' + + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' + + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', + }, + event_id: '$event1', + origin_server_ts: 1507753886000, +}); + +const KEY_BACKUP_DATA = { + first_message_index: 0, + forwarded_count: 0, + is_verified: false, + session_data: { + ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw' + + '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ' + + 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9' + + 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy' + + 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF' + + 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV' + + '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv' + + 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe' + + 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf' + + 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy' + + 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', + mac: '5lxYBHQU80M', + ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', + }, +}; + +function makeTestClient(sessionStore, cryptoStore) { + const scheduler = [ + "getQueueForEvent", "queueEvent", "removeEventFromQueue", + "setProcessFunction", + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); + const store = [ + "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", + "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", + "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", + "storeFilter", "getSyncAccumulator", "startup", "deleteAllData", + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); + store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); + store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); + store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); + return new MatrixClient({ + baseUrl: "https://my.home.server", + idBaseUrl: "https://identity.server", + accessToken: "my.access.token", + request: function() {}, // NOP + store: store, + scheduler: scheduler, + userId: "@alice:bar", + deviceId: "device", + sessionStore: sessionStore, + cryptoStore: cryptoStore, + }); +} + +describe("MegolmBackup", function() { + if (!global.Olm) { + console.warn('Not running megolm backup unit tests: libolm not present'); + return; + } + + let olmDevice; + let mockOlmLib; + let mockCrypto; + let mockStorage; + let sessionStore; + let cryptoStore; + let megolmDecryption; + beforeEach(async function() { + await Olm.init(); + testUtils.beforeEach(this); // eslint-disable-line no-invalid-this + + mockCrypto = testUtils.mock(Crypto, 'Crypto'); + mockCrypto.backupKey = new Olm.PkEncryption(); + mockCrypto.backupKey.set_recipient_key( + "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", + ); + mockCrypto.backupInfo = { + algorithm: "m.megolm_backup.v1", + version: 1, + auth_data: { + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", + }, + }; + + mockStorage = new MockStorageApi(); + sessionStore = new WebStorageSessionStore(mockStorage); + cryptoStore = new MemoryCryptoStore(mockStorage); + + olmDevice = new OlmDevice(sessionStore, cryptoStore); + + // we stub out the olm encryption bits + mockOlmLib = {}; + mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy(); + mockOlmLib.encryptMessageForDevice = + expect.createSpy().andReturn(Promise.resolve()); + }); + + describe("backup", function() { + let mockBaseApis; + let realSetTimeout; + + beforeEach(function() { + mockBaseApis = {}; + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: mockBaseApis, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + + // clobber the setTimeout function to run 100x faster. + // ideally we would use lolex, but we have no oportunity + // to tick the clock between the first try and the retry. + realSetTimeout = global.setTimeout; + global.setTimeout = function(f, n) { + return realSetTimeout(f, n/100); + }; + }); + + afterEach(function() { + global.setTimeout = realSetTimeout; + }); + + it('automatically calls the key back up', function() { + const groupSession = new Olm.OutboundGroupSession(); + groupSession.create(); + + // construct a fake decrypted key event via the use of a mocked + // 'crypto' implementation. + const event = new MatrixEvent({ + type: 'm.room.encrypted', + }); + const decryptedData = { + clearEvent: { + type: 'm.room_key', + content: { + algorithm: 'm.megolm.v1.aes-sha2', + room_id: ROOM_ID, + session_id: groupSession.session_id(), + session_key: groupSession.session_key(), + }, + }, + senderCurve25519Key: "SENDER_CURVE25519", + claimedEd25519Key: "SENDER_ED25519", + }; + + mockCrypto.decryptEvent = function() { + return Promise.resolve(decryptedData); + }; + mockCrypto.cancelRoomKeyRequest = function() {}; + + mockCrypto.backupGroupSession = expect.createSpy(); + + return event.attemptDecryption(mockCrypto).then(() => { + return megolmDecryption.onRoomKeyEvent(event); + }).then(() => { + expect(mockCrypto.backupGroupSession).toHaveBeenCalled(); + }); + }); + + it('sends backups to the server', function() { + const groupSession = new Olm.OutboundGroupSession(); + groupSession.create(); + const ibGroupSession = new Olm.InboundGroupSession(); + ibGroupSession.create(groupSession.session_key()); + + const client = makeTestClient(sessionStore, cryptoStore); + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: client, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + + return client.initCrypto() + .then(() => { + return cryptoStore.doTxn( + "readwrite", + [cryptoStore.STORE_SESSION], + (txn) => { + cryptoStore.addEndToEndInboundGroupSession( + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + groupSession.session_id(), + { + forwardingCurve25519KeyChain: undefined, + keysClaimed: { + ed25519: "SENDER_ED25519", + }, + room_id: ROOM_ID, + session: ibGroupSession.pickle(olmDevice._pickleKey), + }, + txn); + }); + }) + .then(() => { + client.enableKeyBackup({ + algorithm: "m.megolm_backup.v1", + version: 1, + auth_data: { + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", + }, + }); + let numCalls = 0; + return new Promise((resolve, reject) => { + client._http.authedRequest = function( + callback, method, path, queryParams, data, opts, + ) { + ++numCalls; + expect(numCalls).toBeLessThanOrEqualTo(1); + if (numCalls >= 2) { + // exit out of retry loop if there's something wrong + reject(new Error("authedRequest called too many timmes")); + return Promise.resolve({}); + } + expect(method).toBe("PUT"); + expect(path).toBe("/room_keys/keys"); + expect(queryParams.version).toBe(1); + expect(data.rooms[ROOM_ID].sessions).toExist(); + expect(data.rooms[ROOM_ID].sessions).toIncludeKey( + groupSession.session_id(), + ); + resolve(); + return Promise.resolve({}); + }; + client._crypto.backupGroupSession( + "roomId", + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + [], + groupSession.session_id(), + groupSession.session_key(), + ); + }).then(() => { + expect(numCalls).toBe(1); + }); + }); + }); + + it('retries when a backup fails', function() { + const groupSession = new Olm.OutboundGroupSession(); + groupSession.create(); + const ibGroupSession = new Olm.InboundGroupSession(); + ibGroupSession.create(groupSession.session_key()); + + const scheduler = [ + "getQueueForEvent", "queueEvent", "removeEventFromQueue", + "setProcessFunction", + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); + const store = [ + "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", + "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", + "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", + "storeFilter", "getSyncAccumulator", "startup", "deleteAllData", + ].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {}); + store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); + store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); + store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); + const client = new MatrixClient({ + baseUrl: "https://my.home.server", + idBaseUrl: "https://identity.server", + accessToken: "my.access.token", + request: function() {}, // NOP + store: store, + scheduler: scheduler, + userId: "@alice:bar", + deviceId: "device", + sessionStore: sessionStore, + cryptoStore: cryptoStore, + }); + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: client, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + + return client.initCrypto() + .then(() => { + return cryptoStore.doTxn( + "readwrite", + [cryptoStore.STORE_SESSION], + (txn) => { + cryptoStore.addEndToEndInboundGroupSession( + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + groupSession.session_id(), + { + forwardingCurve25519KeyChain: undefined, + keysClaimed: { + ed25519: "SENDER_ED25519", + }, + room_id: ROOM_ID, + session: ibGroupSession.pickle(olmDevice._pickleKey), + }, + txn); + }); + }) + .then(() => { + client.enableKeyBackup({ + algorithm: "foobar", + version: 1, + auth_data: { + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", + }, + }); + let numCalls = 0; + return new Promise((resolve, reject) => { + client._http.authedRequest = function( + callback, method, path, queryParams, data, opts, + ) { + ++numCalls; + expect(numCalls).toBeLessThanOrEqualTo(2); + if (numCalls >= 3) { + // exit out of retry loop if there's something wrong + reject(new Error("authedRequest called too many timmes")); + return Promise.resolve({}); + } + expect(method).toBe("PUT"); + expect(path).toBe("/room_keys/keys"); + expect(queryParams.version).toBe(1); + expect(data.rooms[ROOM_ID].sessions).toExist(); + expect(data.rooms[ROOM_ID].sessions).toIncludeKey( + groupSession.session_id(), + ); + if (numCalls > 1) { + resolve(); + return Promise.resolve({}); + } else { + return Promise.reject( + new Error("this is an expected failure"), + ); + } + }; + client._crypto.backupGroupSession( + "roomId", + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + [], + groupSession.session_id(), + groupSession.session_key(), + ); + }).then(() => { + expect(numCalls).toBe(2); + }); + }); + }); + }); + + describe("restore", function() { + let client; + + beforeEach(function() { + client = makeTestClient(sessionStore, cryptoStore); + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: client, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + + return client.initCrypto(); + }); + + afterEach(function() { + client.stopClient(); + }); + + it('can restore from backup', function() { + client._http.authedRequest = function() { + return Promise.resolve(KEY_BACKUP_DATA); + }; + return client.restoreKeyBackupWithRecoveryKey( + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + ROOM_ID, + SESSION_ID, + ).then(() => { + return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); + }).then((res) => { + expect(res.clearEvent.content).toEqual('testytest'); + }); + }); + + it('can restore backup by room', function() { + client._http.authedRequest = function() { + return Promise.resolve({ + rooms: { + [ROOM_ID]: { + sessions: { + [SESSION_ID]: KEY_BACKUP_DATA, + }, + }, + }, + }); + }; + return client.restoreKeyBackupWithRecoveryKey( + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + ).then(() => { + return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); + }).then((res) => { + expect(res.clearEvent.content).toEqual('testytest'); + }); + }); + }); +}); diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index 8ef22f6b0..a59e0af43 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -139,6 +139,9 @@ describe("MatrixClient", function() { store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); + store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null)); + store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null)); + store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true)); client = new MatrixClient({ baseUrl: "https://my.home.server", idBaseUrl: identityServerUrl, @@ -182,7 +185,7 @@ describe("MatrixClient", function() { }); }); - it("should not POST /filter if a matching filter already exists", function(done) { + it("should not POST /filter if a matching filter already exists", async function() { httpLookups = []; httpLookups.push(PUSH_RULES_RESPONSE); httpLookups.push(SYNC_RESPONSE); @@ -191,15 +194,19 @@ describe("MatrixClient", function() { const filter = new sdk.Filter(0, filterId); filter.setDefinition({"room": {"timeline": {"limit": 8}}}); store.getFilter.andReturn(filter); - client.startClient(); - - client.on("sync", function syncListener(state) { - if (state === "SYNCING") { - expect(httpLookups.length).toEqual(0); - client.removeListener("sync", syncListener); - done(); - } + const syncPromise = new Promise((resolve, reject) => { + client.on("sync", function syncListener(state) { + if (state === "SYNCING") { + expect(httpLookups.length).toEqual(0); + client.removeListener("sync", syncListener); + resolve(); + } else if (state === "ERROR") { + reject(new Error("sync error")); + } + }); }); + await client.startClient(); + await syncPromise; }); describe("getSyncState", function() { @@ -207,15 +214,18 @@ describe("MatrixClient", function() { expect(client.getSyncState()).toBe(null); }); - it("should return the same sync state as emitted sync events", function(done) { - client.on("sync", function syncListener(state) { - expect(state).toEqual(client.getSyncState()); - if (state === "SYNCING") { - client.removeListener("sync", syncListener); - done(); - } + it("should return the same sync state as emitted sync events", async function() { + const syncingPromise = new Promise((resolve) => { + client.on("sync", function syncListener(state) { + expect(state).toEqual(client.getSyncState()); + if (state === "SYNCING") { + client.removeListener("sync", syncListener); + resolve(); + } + }); }); - client.startClient(); + await client.startClient(); + await syncingPromise; }); }); @@ -258,8 +268,8 @@ describe("MatrixClient", function() { }); describe("retryImmediately", function() { - it("should return false if there is no request waiting", function() { - client.startClient(); + it("should return false if there is no request waiting", async function() { + await client.startClient(); expect(client.retryImmediately()).toBe(false); }); @@ -380,7 +390,7 @@ describe("MatrixClient", function() { client.startClient(); }); - it("should transition ERROR -> PREPARED after /sync if prev failed", + it("should transition ERROR -> CATCHUP after /sync if prev failed", function(done) { const expectedStates = []; acceptKeepalives = false; @@ -403,7 +413,7 @@ describe("MatrixClient", function() { expectedStates.push(["RECONNECTING", null]); expectedStates.push(["ERROR", "RECONNECTING"]); - expectedStates.push(["PREPARED", "ERROR"]); + expectedStates.push(["CATCHUP", "ERROR"]); client.on("sync", syncChecker(expectedStates, done)); client.startClient(); }); diff --git a/spec/unit/room-member.spec.js b/spec/unit/room-member.spec.js index c36bb281c..298771128 100644 --- a/spec/unit/room-member.spec.js +++ b/spec/unit/room-member.spec.js @@ -192,6 +192,15 @@ describe("RoomMember", function() { }); }); + describe("isOutOfBand", function() { + it("should be set by markOutOfBand", function() { + const member = new RoomMember(); + expect(member.isOutOfBand()).toEqual(false); + member.markOutOfBand(); + expect(member.isOutOfBand()).toEqual(true); + }); + }); + describe("setMembershipEvent", function() { const joinEvent = utils.mkMembership({ event: true, diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 21eafba4a..baa602781 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -11,6 +11,9 @@ describe("RoomState", function() { const roomId = "!foo:bar"; const userA = "@alice:bar"; const userB = "@bob:bar"; + const userC = "@cleo:bar"; + const userLazy = "@lazy:bar"; + let state; beforeEach(function() { @@ -78,8 +81,8 @@ describe("RoomState", function() { }); describe("getSentinelMember", function() { - it("should return null if there is no member", function() { - expect(state.getSentinelMember("@no-one:here")).toEqual(null); + it("should return a member with the user id as name", function() { + expect(state.getSentinelMember("@no-one:here").name).toEqual("@no-one:here"); }); it("should return a member which doesn't change when the state is updated", @@ -162,6 +165,7 @@ describe("RoomState", function() { ]; let emitCount = 0; state.on("RoomState.newMember", function(ev, st, mem) { + expect(state.getMember(mem.userId)).toEqual(mem); expect(mem.userId).toEqual(memberEvents[emitCount].getSender()); expect(mem.membership).toBeFalsy(); // not defined yet emitCount += 1; @@ -222,7 +226,6 @@ describe("RoomState", function() { it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function() { - const userC = "@cleo:bar"; const memberEvent = utils.mkMembership({ mship: "join", user: userC, room: roomId, event: true, }); @@ -262,6 +265,114 @@ describe("RoomState", function() { }); }); + describe("setOutOfBandMembers", function() { + it("should add a new member", function() { + const oobMemberEvent = utils.mkMembership({ + user: userLazy, mship: "join", room: roomId, event: true, + }); + state.markOutOfBandMembersStarted(); + state.setOutOfBandMembers([oobMemberEvent]); + const member = state.getMember(userLazy); + expect(member.userId).toEqual(userLazy); + expect(member.isOutOfBand()).toEqual(true); + }); + + it("should have no effect when not in correct status", function() { + state.setOutOfBandMembers([utils.mkMembership({ + user: userLazy, mship: "join", room: roomId, event: true, + })]); + expect(state.getMember(userLazy)).toBeFalsy(); + }); + + it("should emit newMember when adding a member", function() { + const userLazy = "@oob:hs"; + const oobMemberEvent = utils.mkMembership({ + user: userLazy, mship: "join", room: roomId, event: true, + }); + let eventReceived = false; + state.once('RoomState.newMember', (_, __, member) => { + expect(member.userId).toEqual(userLazy); + eventReceived = true; + }); + state.markOutOfBandMembersStarted(); + state.setOutOfBandMembers([oobMemberEvent]); + expect(eventReceived).toEqual(true); + }); + + it("should never overwrite existing members", function() { + const oobMemberEvent = utils.mkMembership({ + user: userA, mship: "join", room: roomId, event: true, + }); + state.markOutOfBandMembersStarted(); + state.setOutOfBandMembers([oobMemberEvent]); + const memberA = state.getMember(userA); + expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId()); + expect(memberA.isOutOfBand()).toEqual(false); + }); + + it("should emit members when updating a member", function() { + const doesntExistYetUserId = "@doesntexistyet:hs"; + const oobMemberEvent = utils.mkMembership({ + user: doesntExistYetUserId, mship: "join", room: roomId, event: true, + }); + let eventReceived = false; + state.once('RoomState.members', (_, __, member) => { + expect(member.userId).toEqual(doesntExistYetUserId); + eventReceived = true; + }); + + state.markOutOfBandMembersStarted(); + state.setOutOfBandMembers([oobMemberEvent]); + expect(eventReceived).toEqual(true); + }); + }); + + describe("clone", function() { + it("should contain same information as original", function() { + // include OOB members in copy + state.markOutOfBandMembersStarted(); + state.setOutOfBandMembers([utils.mkMembership({ + user: userLazy, mship: "join", room: roomId, event: true, + })]); + const copy = state.clone(); + // check individual members + [userA, userB, userLazy].forEach((userId) => { + const member = state.getMember(userId); + const memberCopy = copy.getMember(userId); + expect(member.name).toEqual(memberCopy.name); + expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand()); + }); + // check member keys + expect(Object.keys(state.members)).toEqual(Object.keys(copy.members)); + // check join count + expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount()); + }); + + it("should mark old copy as not waiting for out of band anymore", function() { + state.markOutOfBandMembersStarted(); + const copy = state.clone(); + copy.setOutOfBandMembers([utils.mkMembership({ + user: userA, mship: "join", room: roomId, event: true, + })]); + // should have no effect as it should be marked in status finished just like copy + state.setOutOfBandMembers([utils.mkMembership({ + user: userLazy, mship: "join", room: roomId, event: true, + })]); + expect(state.getMember(userLazy)).toBeFalsy(); + }); + + it("should return copy independent of original", function() { + const copy = state.clone(); + copy.setStateEvents([utils.mkMembership({ + user: userLazy, mship: "join", room: roomId, event: true, + })]); + + expect(state.getMember(userLazy)).toBeFalsy(); + expect(state.getJoinedMemberCount()).toEqual(2); + expect(copy.getJoinedMemberCount()).toEqual(3); + }); + }); + describe("setTypingEvent", function() { it("should call setTypingEvent on each RoomMember", function() { const typingEvent = utils.mkEvent({ @@ -284,13 +395,6 @@ describe("RoomState", function() { }); describe("maySendStateEvent", function() { - it("should say non-joined members may not send state", - function() { - expect(state.maySendStateEvent( - 'm.room.name', "@nobody:nowhere", - )).toEqual(false); - }); - it("should say any member may send state with no power level event", function() { expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true); @@ -366,15 +470,117 @@ describe("RoomState", function() { }); }); - describe("maySendEvent", function() { - it("should say non-joined members may not send events", - function() { - expect(state.maySendEvent( - 'm.room.message', "@nobody:nowhere", - )).toEqual(false); - expect(state.maySendMessage("@nobody:nowhere")).toEqual(false); + describe("getJoinedMemberCount", function() { + beforeEach(() => { + state = new RoomState(roomId); }); + it("should update after adding joined member", function() { + state.setStateEvents([ + utils.mkMembership({event: true, mship: "join", + user: userA, room: roomId}), + ]); + expect(state.getJoinedMemberCount()).toEqual(1); + state.setStateEvents([ + utils.mkMembership({event: true, mship: "join", + user: userC, room: roomId}), + ]); + expect(state.getJoinedMemberCount()).toEqual(2); + }); + }); + + describe("getInvitedMemberCount", function() { + beforeEach(() => { + state = new RoomState(roomId); + }); + + it("should update after adding invited member", function() { + state.setStateEvents([ + utils.mkMembership({event: true, mship: "invite", + user: userA, room: roomId}), + ]); + expect(state.getInvitedMemberCount()).toEqual(1); + state.setStateEvents([ + utils.mkMembership({event: true, mship: "invite", + user: userC, room: roomId}), + ]); + expect(state.getInvitedMemberCount()).toEqual(2); + }); + }); + + describe("setJoinedMemberCount", function() { + beforeEach(() => { + state = new RoomState(roomId); + }); + + it("should, once used, override counting members from state", function() { + state.setStateEvents([ + utils.mkMembership({event: true, mship: "join", + user: userA, room: roomId}), + ]); + expect(state.getJoinedMemberCount()).toEqual(1); + state.setJoinedMemberCount(100); + expect(state.getJoinedMemberCount()).toEqual(100); + state.setStateEvents([ + utils.mkMembership({event: true, mship: "join", + user: userC, room: roomId}), + ]); + expect(state.getJoinedMemberCount()).toEqual(100); + }); + + it("should, once used, override counting members from state, " + + "also after clone", function() { + state.setStateEvents([ + utils.mkMembership({event: true, mship: "join", + user: userA, room: roomId}), + ]); + state.setJoinedMemberCount(100); + const copy = state.clone(); + copy.setStateEvents([ + utils.mkMembership({event: true, mship: "join", + user: userC, room: roomId}), + ]); + expect(state.getJoinedMemberCount()).toEqual(100); + }); + }); + + describe("setInvitedMemberCount", function() { + beforeEach(() => { + state = new RoomState(roomId); + }); + + it("should, once used, override counting members from state", function() { + state.setStateEvents([ + utils.mkMembership({event: true, mship: "invite", + user: userB, room: roomId}), + ]); + expect(state.getInvitedMemberCount()).toEqual(1); + state.setInvitedMemberCount(100); + expect(state.getInvitedMemberCount()).toEqual(100); + state.setStateEvents([ + utils.mkMembership({event: true, mship: "invite", + user: userC, room: roomId}), + ]); + expect(state.getInvitedMemberCount()).toEqual(100); + }); + + it("should, once used, override counting members from state, " + + "also after clone", function() { + state.setStateEvents([ + utils.mkMembership({event: true, mship: "invite", + user: userB, room: roomId}), + ]); + state.setInvitedMemberCount(100); + const copy = state.clone(); + copy.setStateEvents([ + utils.mkMembership({event: true, mship: "invite", + user: userC, room: roomId}), + ]); + expect(state.getInvitedMemberCount()).toEqual(100); + }); + }); + + describe("maySendEvent", function() { it("should say any member may send events with no power level event", function() { expect(state.maySendEvent('m.room.message', userA)).toEqual(true); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 793a6b5e7..4a8ea7e59 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -67,13 +67,14 @@ describe("Room", function() { describe("getMember", function() { beforeEach(function() { - // clobber members property with test data - room.currentState.members = { - "@alice:bar": { - userId: userA, - roomId: roomId, - }, - }; + room.currentState.getMember.andCall(function(userId) { + return { + "@alice:bar": { + userId: userA, + roomId: roomId, + }, + }[userId]; + }); }); it("should return null if the member isn't in current state", function() { @@ -386,7 +387,7 @@ describe("Room", function() { let events = null; beforeEach(function() { - room = new Room(roomId, {timelineSupport: timelineSupport}); + room = new Room(roomId, null, null, {timelineSupport: timelineSupport}); // set events each time to avoid resusing Event objects (which // doesn't work because they get frozen) events = [ @@ -468,7 +469,7 @@ describe("Room", function() { describe("compareEventOrdering", function() { beforeEach(function() { - room = new Room(roomId, {timelineSupport: true}); + room = new Room(roomId, null, null, {timelineSupport: true}); }); const events = [ @@ -570,72 +571,75 @@ describe("Room", function() { describe("hasMembershipState", function() { it("should return true for a matching userId and membership", function() { - room.currentState.members = { - "@alice:bar": { userId: "@alice:bar", membership: "join" }, - "@bob:bar": { userId: "@bob:bar", membership: "invite" }, - }; + room.currentState.getMember.andCall(function(userId) { + return { + "@alice:bar": { userId: "@alice:bar", membership: "join" }, + "@bob:bar": { userId: "@bob:bar", membership: "invite" }, + }[userId]; + }); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true); }); it("should return false if match membership but no match userId", function() { - room.currentState.members = { - "@alice:bar": { userId: "@alice:bar", membership: "join" }, - }; + room.currentState.getMember.andCall(function(userId) { + return { + "@alice:bar": { userId: "@alice:bar", membership: "join" }, + }[userId]; + }); expect(room.hasMembershipState("@bob:bar", "join")).toBe(false); }); it("should return false if match userId but no match membership", function() { - room.currentState.members = { - "@alice:bar": { userId: "@alice:bar", membership: "join" }, - }; + room.currentState.getMember.andCall(function(userId) { + return { + "@alice:bar": { userId: "@alice:bar", membership: "join" }, + }[userId]; + }); expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false); }); it("should return false if no match membership or userId", function() { - room.currentState.members = { - "@alice:bar": { userId: "@alice:bar", membership: "join" }, - }; + room.currentState.getMember.andCall(function(userId) { + return { + "@alice:bar": { userId: "@alice:bar", membership: "join" }, + }[userId]; + }); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false); }); it("should return false if no members exist", function() { - room.currentState.members = {}; expect(room.hasMembershipState("@foo:bar", "join")).toBe(false); }); }); describe("recalculate", function() { - let stateLookup = { - // event.type + "$" event.state_key : MatrixEvent - }; - const setJoinRule = function(rule) { - stateLookup["m.room.join_rules$"] = utils.mkEvent({ + room.addLiveEvents([utils.mkEvent({ type: "m.room.join_rules", room: roomId, user: userA, content: { join_rule: rule, }, event: true, - }); + })]); }; const setAliases = function(aliases, stateKey) { if (!stateKey) { stateKey = "flibble"; } - stateLookup["m.room.aliases$" + stateKey] = utils.mkEvent({ + room.addLiveEvents([utils.mkEvent({ type: "m.room.aliases", room: roomId, skey: stateKey, content: { aliases: aliases, }, event: true, - }); + })]); }; const setRoomName = function(name) { - stateLookup["m.room.name$"] = utils.mkEvent({ + room.addLiveEvents([utils.mkEvent({ type: "m.room.name", room: roomId, user: userA, content: { name: name, }, event: true, - }); + })]); }; const addMember = function(userId, state, opts) { if (!state) { @@ -647,56 +651,14 @@ describe("Room", function() { opts.user = opts.user || userId; opts.skey = userId; opts.event = true; - stateLookup["m.room.member$" + userId] = utils.mkMembership(opts); + const event = utils.mkMembership(opts); + room.addLiveEvents([event]); + return event; }; beforeEach(function() { - stateLookup = {}; - room.currentState.getStateEvents.andCall(function(type, key) { - if (key === undefined) { - const prefix = type + "$"; - const list = []; - for (const stateBlob in stateLookup) { - if (!stateLookup.hasOwnProperty(stateBlob)) { - continue; - } - if (stateBlob.indexOf(prefix) === 0) { - list.push(stateLookup[stateBlob]); - } - } - return list; - } else { - return stateLookup[type + "$" + key]; - } - }); - room.currentState.getMembers.andCall(function() { - const memberEvents = room.currentState.getStateEvents("m.room.member"); - const members = []; - for (let i = 0; i < memberEvents.length; i++) { - members.push({ - name: memberEvents[i].event.content && - memberEvents[i].event.content.displayname ? - memberEvents[i].event.content.displayname : - memberEvents[i].getStateKey(), - userId: memberEvents[i].getStateKey(), - events: { member: memberEvents[i] }, - }); - } - return members; - }); - room.currentState.getMember.andCall(function(userId) { - const memberEvent = room.currentState.getStateEvents( - "m.room.member", userId, - ); - return { - name: memberEvent.event.content && - memberEvent.event.content.displayname ? - memberEvent.event.content.displayname : - memberEvent.getStateKey(), - userId: memberEvent.getStateKey(), - events: { member: memberEvent }, - }; - }); + // no mocking + room = new Room(roomId, null, userA); }); describe("Room.recalculate => Stripped State Events", function() { @@ -704,8 +666,8 @@ describe("Room", function() { "room is an invite room", function() { const roomName = "flibble"; - addMember(userA, "invite"); - stateLookup["m.room.member$" + userA].event.invite_room_state = [ + const event = addMember(userA, "invite"); + event.event.invite_room_state = [ { type: "m.room.name", state_key: "", @@ -715,30 +677,108 @@ describe("Room", function() { }, ]; - room.recalculate(userA); - expect(room.currentState.setStateEvents).toHaveBeenCalled(); - // first call, first arg (which is an array), first element in array - const fakeEvent = room.currentState.setStateEvents.calls[0]. - arguments[0][0]; - expect(fakeEvent.getContent()).toEqual({ - name: roomName, - }); + room.recalculate(); + expect(room.name).toEqual(roomName); }); it("should not clobber state events if it isn't an invite room", function() { - addMember(userA, "join"); - stateLookup["m.room.member$" + userA].event.invite_room_state = [ + const event = addMember(userA, "join"); + const roomName = "flibble"; + setRoomName(roomName); + const roomNameToIgnore = "ignoreme"; + event.event.invite_room_state = [ { type: "m.room.name", state_key: "", content: { - name: "flibble", + name: roomNameToIgnore, }, }, ]; - room.recalculate(userA); - expect(room.currentState.setStateEvents).toNotHaveBeenCalled(); + room.recalculate(); + expect(room.name).toEqual(roomName); + }); + }); + + describe("Room.recalculate => Room Name using room summary", function() { + it("should use room heroes if available", function() { + addMember(userA, "invite"); + addMember(userB); + addMember(userC); + addMember(userD); + room.setSummary({ + "m.heroes": [userB, userC, userD], + }); + + room.recalculate(); + expect(room.name).toEqual(`${userB} and 2 others`); + }); + + it("missing hero member state reverts to mxid", function() { + room.setSummary({ + "m.heroes": [userB], + "m.joined_member_count": 2, + }); + + room.recalculate(); + expect(room.name).toEqual(userB); + }); + + it("uses hero name from state", function() { + const name = "Mr B"; + addMember(userA, "invite"); + addMember(userB, "join", {name}); + room.setSummary({ + "m.heroes": [userB], + }); + + room.recalculate(); + expect(room.name).toEqual(name); + }); + + it("uses counts from summary", function() { + const name = "Mr B"; + addMember(userB, "join", {name}); + room.setSummary({ + "m.heroes": [userB], + "m.joined_member_count": 50, + "m.invited_member_count": 50, + }); + room.recalculate(); + expect(room.name).toEqual(`${name} and 98 others`); + }); + + it("relies on heroes in case of absent counts", function() { + const nameB = "Mr Bean"; + const nameC = "Mel C"; + addMember(userB, "join", {name: nameB}); + addMember(userC, "join", {name: nameC}); + room.setSummary({ + "m.heroes": [userB, userC], + }); + room.recalculate(); + expect(room.name).toEqual(`${nameB} and ${nameC}`); + }); + + it("uses only heroes", function() { + const nameB = "Mr Bean"; + addMember(userB, "join", {name: nameB}); + addMember(userC, "join"); + room.setSummary({ + "m.heroes": [userB], + }); + room.recalculate(); + expect(room.name).toEqual(nameB); + }); + + it("reverts to empty room in case of self chat", function() { + room.setSummary({ + "m.heroes": [], + "m.invited_member_count": 1, + }); + room.recalculate(); + expect(room.name).toEqual("Empty room"); }); }); @@ -751,7 +791,7 @@ describe("Room", function() { addMember(userB); addMember(userC); addMember(userD); - room.recalculate(userA); + room.recalculate(); const name = room.name; // we expect at least 1 member to be mentioned const others = [userB, userC, userD]; @@ -772,7 +812,7 @@ describe("Room", function() { addMember(userA); addMember(userB); addMember(userC); - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name.indexOf(userB)).toNotEqual(-1, name); expect(name.indexOf(userC)).toNotEqual(-1, name); @@ -785,7 +825,7 @@ describe("Room", function() { addMember(userA); addMember(userB); addMember(userC); - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name.indexOf(userB)).toNotEqual(-1, name); expect(name.indexOf(userC)).toNotEqual(-1, name); @@ -797,7 +837,7 @@ describe("Room", function() { setJoinRule("public"); addMember(userA); addMember(userB); - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name.indexOf(userB)).toNotEqual(-1, name); }); @@ -808,7 +848,7 @@ describe("Room", function() { setJoinRule("invite"); addMember(userA); addMember(userB); - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name.indexOf(userB)).toNotEqual(-1, name); }); @@ -818,7 +858,7 @@ describe("Room", function() { setJoinRule("invite"); addMember(userA, "invite", {user: userB}); addMember(userB); - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name.indexOf(userB)).toNotEqual(-1, name); }); @@ -828,7 +868,7 @@ describe("Room", function() { const alias = "#room_alias:here"; setJoinRule("invite"); setAliases([alias, "#another:one"]); - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name).toEqual(alias); }); @@ -838,7 +878,7 @@ describe("Room", function() { const alias = "#room_alias:here"; setJoinRule("public"); setAliases([alias, "#another:one"]); - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name).toEqual(alias); }); @@ -848,7 +888,7 @@ describe("Room", function() { const roomName = "A mighty name indeed"; setJoinRule("invite"); setRoomName(roomName); - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name).toEqual(roomName); }); @@ -858,25 +898,23 @@ describe("Room", function() { const roomName = "A mighty name indeed"; setJoinRule("public"); setRoomName(roomName); - room.recalculate(userA); - const name = room.name; - expect(name).toEqual(roomName); + room.recalculate(); + expect(room.name).toEqual(roomName); }); it("should return 'Empty room' for private (invite join_rules) rooms if" + " a room name and alias don't exist and it is a self-chat.", function() { setJoinRule("invite"); addMember(userA); - room.recalculate(userA); - const name = room.name; - expect(name).toEqual("Empty room"); + room.recalculate(); + expect(room.name).toEqual("Empty room"); }); it("should return 'Empty room' for public (public join_rules) rooms if a" + " room name and alias don't exist and it is a self-chat.", function() { setJoinRule("public"); addMember(userA); - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name).toEqual("Empty room"); }); @@ -884,7 +922,7 @@ describe("Room", function() { it("should return 'Empty room' if there is no name, " + "alias or members in the room.", function() { - room.recalculate(userA); + room.recalculate(); const name = room.name; expect(name).toEqual("Empty room"); }); @@ -893,9 +931,9 @@ describe("Room", function() { "available", function() { setJoinRule("invite"); - addMember(userA, 'join', {name: "Alice"}); - addMember(userB, "invite", {user: userA}); - room.recalculate(userB); + addMember(userB, 'join', {name: "Alice"}); + addMember(userA, "invite", {user: userA}); + room.recalculate(); const name = room.name; expect(name).toEqual("Alice"); }); @@ -903,11 +941,11 @@ describe("Room", function() { it("should return inviter mxid if display name not available", function() { setJoinRule("invite"); - addMember(userA); - addMember(userB, "invite", {user: userA}); - room.recalculate(userB); + addMember(userB); + addMember(userA, "invite", {user: userA}); + room.recalculate(); const name = room.name; - expect(name).toEqual(userA); + expect(name).toEqual(userB); }); }); }); @@ -1154,7 +1192,7 @@ describe("Room", function() { describe("addPendingEvent", function() { it("should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", function() { - const room = new Room(roomId, { + const room = new Room(roomId, null, userA, { pendingEventOrdering: "detached", }); const eventA = utils.mkMessage({ @@ -1180,7 +1218,7 @@ describe("Room", function() { it("should add pending events to the timeline if " + "pendingEventOrdering == 'chronological'", function() { - room = new Room(roomId, { + room = new Room(roomId, null, userA, { pendingEventOrdering: "chronological", }); const eventA = utils.mkMessage({ @@ -1204,7 +1242,7 @@ describe("Room", function() { describe("updatePendingEvent", function() { it("should remove cancelled events from the pending list", function() { - const room = new Room(roomId, { + const room = new Room(roomId, null, userA, { pendingEventOrdering: "detached", }); const eventA = utils.mkMessage({ @@ -1240,7 +1278,7 @@ describe("Room", function() { it("should remove cancelled events from the timeline", function() { - const room = new Room(roomId); + const room = new Room(roomId, null, userA); const eventA = utils.mkMessage({ room: roomId, user: userA, event: true, }); @@ -1272,4 +1310,153 @@ describe("Room", function() { expect(callCount).toEqual(1); }); }); + + describe("loadMembersIfNeeded", function() { + function createClientMock(serverResponse, storageResponse = null) { + return { + getEventMapper: function() { + // events should already be MatrixEvents + return function(event) {return event;}; + }, + isRoomEncrypted: function() { + return false; + }, + _http: { + serverResponse, + authedRequest: function() { + if (this.serverResponse instanceof Error) { + return Promise.reject(this.serverResponse); + } else { + return Promise.resolve({chunk: this.serverResponse}); + } + }, + }, + store: { + storageResponse, + storedMembers: null, + getOutOfBandMembers: function() { + if (this.storageResponse instanceof Error) { + return Promise.reject(this.storageResponse); + } else { + return Promise.resolve(this.storageResponse); + } + }, + setOutOfBandMembers: function(roomId, memberEvents) { + this.storedMembers = memberEvents; + return Promise.resolve(); + }, + getSyncToken: () => "sync_token", + }, + }; + } + + const memberEvent = utils.mkMembership({ + user: "@user_a:bar", mship: "join", + room: roomId, event: true, name: "User A", + }); + + it("should load members from server on first call", async function() { + const client = createClientMock([memberEvent]); + const room = new Room(roomId, client, null, {lazyLoadMembers: true}); + await room.loadMembersIfNeeded(); + const memberA = room.getMember("@user_a:bar"); + expect(memberA.name).toEqual("User A"); + const storedMembers = client.store.storedMembers; + expect(storedMembers.length).toEqual(1); + expect(storedMembers[0].event_id).toEqual(memberEvent.getId()); + }); + + it("should take members from storage if available", async function() { + const memberEvent2 = utils.mkMembership({ + user: "@user_a:bar", mship: "join", + room: roomId, event: true, name: "Ms A", + }); + const client = createClientMock([memberEvent2], [memberEvent]); + const room = new Room(roomId, client, null, {lazyLoadMembers: true}); + + await room.loadMembersIfNeeded(); + + const memberA = room.getMember("@user_a:bar"); + expect(memberA.name).toEqual("User A"); + }); + + it("should allow retry on error", async function() { + const client = createClientMock(new Error("server says no")); + const room = new Room(roomId, client, null, {lazyLoadMembers: true}); + let hasThrown = false; + try { + await room.loadMembersIfNeeded(); + } catch(err) { + hasThrown = true; + } + expect(hasThrown).toEqual(true); + + client._http.serverResponse = [memberEvent]; + await room.loadMembersIfNeeded(); + const memberA = room.getMember("@user_a:bar"); + expect(memberA.name).toEqual("User A"); + }); + }); + + describe("getMyMembership", function() { + it("should return synced membership if membership isn't available yet", + function() { + const room = new Room(roomId, null, userA); + room.updateMyMembership("invite"); + expect(room.getMyMembership()).toEqual("invite"); + }); + it("should emit a Room.myMembership event on a change", + function() { + const room = new Room(roomId, null, userA); + const events = []; + room.on("Room.myMembership", (_room, membership, oldMembership) => { + events.push({membership, oldMembership}); + }); + room.updateMyMembership("invite"); + expect(room.getMyMembership()).toEqual("invite"); + expect(events[0]).toEqual({membership: "invite", oldMembership: null}); + events.splice(0); //clear + room.updateMyMembership("invite"); + expect(events.length).toEqual(0); + room.updateMyMembership("join"); + expect(room.getMyMembership()).toEqual("join"); + expect(events[0]).toEqual({membership: "join", oldMembership: "invite"}); + }); + }); + + describe("guessDMUserId", function() { + it("should return first hero id", + function() { + const room = new Room(roomId, null, userA); + room.setSummary({'m.heroes': [userB]}); + expect(room.guessDMUserId()).toEqual(userB); + }); + it("should return first member that isn't self", + function() { + const room = new Room(roomId, null, userA); + room.addLiveEvents([utils.mkMembership({ + user: userB, mship: "join", + room: roomId, event: true, + })]); + expect(room.guessDMUserId()).toEqual(userB); + }); + it("should return self if only member present", + function() { + const room = new Room(roomId, null, userA); + expect(room.guessDMUserId()).toEqual(userA); + }); + }); + + describe("maySendMessage", function() { + it("should return false if synced membership not join", + function() { + const room = new Room(roomId, null, userA); + room.updateMyMembership("invite"); + expect(room.maySendMessage()).toEqual(false); + room.updateMyMembership("leave"); + expect(room.maySendMessage()).toEqual(false); + room.updateMyMembership("join"); + expect(room.maySendMessage()).toEqual(true); + }); + }); }); diff --git a/spec/unit/sync-accumulator.spec.js b/spec/unit/sync-accumulator.spec.js index 1a857fe65..a29be7efd 100644 --- a/spec/unit/sync-accumulator.spec.js +++ b/spec/unit/sync-accumulator.spec.js @@ -52,6 +52,11 @@ describe("SyncAccumulator", function() { member("bob", "join"), ], }, + summary: { + "m.heroes": undefined, + "m.joined_member_count": undefined, + "m.invited_member_count": undefined, + }, timeline: { events: [msg("alice", "hi")], prev_batch: "something", @@ -318,6 +323,58 @@ describe("SyncAccumulator", function() { }, }); }); + + describe("summary field", function() { + function createSyncResponseWithSummary(summary) { + return { + next_batch: "abc", + rooms: { + invite: {}, + leave: {}, + join: { + "!foo:bar": { + account_data: { events: [] }, + ephemeral: { events: [] }, + unread_notifications: {}, + state: { + events: [], + }, + summary: summary, + timeline: { + events: [], + prev_batch: "something", + }, + }, + }, + }, + }; + } + + it("should copy summary properties", function() { + sa.accumulate(createSyncResponseWithSummary({ + "m.heroes": ["@alice:bar"], + "m.invited_member_count": 2, + })); + const summary = sa.getJSON().roomsData.join["!foo:bar"].summary; + expect(summary["m.invited_member_count"]).toEqual(2); + expect(summary["m.heroes"]).toEqual(["@alice:bar"]); + }); + + it("should accumulate summary properties", function() { + sa.accumulate(createSyncResponseWithSummary({ + "m.heroes": ["@alice:bar"], + "m.invited_member_count": 2, + })); + sa.accumulate(createSyncResponseWithSummary({ + "m.heroes": ["@bob:bar"], + "m.joined_member_count": 5, + })); + const summary = sa.getJSON().roomsData.join["!foo:bar"].summary; + expect(summary["m.invited_member_count"]).toEqual(2); + expect(summary["m.joined_member_count"]).toEqual(5); + expect(summary["m.heroes"]).toEqual(["@bob:bar"]); + }); + }); }); function syncSkeleton(joinObj) { diff --git a/src/autodiscovery.js b/src/autodiscovery.js new file mode 100644 index 000000000..6ce4392e2 --- /dev/null +++ b/src/autodiscovery.js @@ -0,0 +1,390 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** @module auto-discovery */ + +import Promise from 'bluebird'; +const logger = require("./logger"); +import { URL as NodeURL } from "url"; + +// Dev note: Auto discovery is part of the spec. +// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + +/** + * Description for what an automatically discovered client configuration + * would look like. Although this is a class, it is recommended that it + * be treated as an interface definition rather than as a class. + * + * Additional properties than those defined here may be present, and + * should follow the Java package naming convention. + */ +class DiscoveredClientConfig { // eslint-disable-line no-unused-vars + // Dev note: this is basically a copy/paste of the .well-known response + // object as defined in the spec. It does have additional information, + // however. Overall, this exists to serve as a place for documentation + // and not functionality. + // See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client + + constructor() { + /** + * The homeserver configuration the client should use. This will + * always be present on the object. + * @type {{state: string, base_url: string}} The configuration. + */ + this["m.homeserver"] = { + /** + * The lookup result state. If this is anything other than + * AutoDiscovery.SUCCESS then base_url may be falsey. Additionally, + * if this is not AutoDiscovery.SUCCESS then the client should + * assume the other properties in the client config (such as + * the identity server configuration) are not valid. + */ + state: AutoDiscovery.PROMPT, + + /** + * If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT + * then this will contain a human-readable (English) message + * for what went wrong. If the state is none of those previously + * mentioned, this will be falsey. + */ + error: "Something went wrong", + + /** + * The base URL clients should use to talk to the homeserver, + * particularly for the login process. May be falsey if the + * state is not AutoDiscovery.SUCCESS. + */ + base_url: "https://matrix.org", + }; + + /** + * The identity server configuration the client should use. This + * will always be present on teh object. + * @type {{state: string, base_url: string}} The configuration. + */ + this["m.identity_server"] = { + /** + * The lookup result state. If this is anything other than + * AutoDiscovery.SUCCESS then base_url may be falsey. + */ + state: AutoDiscovery.PROMPT, + + /** + * The base URL clients should use for interacting with the + * identity server. May be falsey if the state is not + * AutoDiscovery.SUCCESS. + */ + base_url: "https://vector.im", + }; + } +} + +/** + * Utilities for automatically discovery resources, such as homeservers + * for users to log in to. + */ +export class AutoDiscovery { + + // Dev note: the constants defined here are related to but not + // exactly the same as those in the spec. This is to hopefully + // translate the meaning of the states in the spec, but also + // support our own if needed. + + /** + * The auto discovery failed. The client is expected to communicate + * the error to the user and refuse logging in. + * @return {string} + * @constructor + */ + static get FAIL_ERROR() { return "FAIL_ERROR"; } + + /** + * The auto discovery failed, however the client may still recover + * from the problem. The client is recommended to that the same + * action it would for PROMPT while also warning the user about + * what went wrong. The client may also treat this the same as + * a FAIL_ERROR state. + * @return {string} + * @constructor + */ + static get FAIL_PROMPT() { return "FAIL_PROMPT"; } + + /** + * The auto discovery didn't fail but did not find anything of + * interest. The client is expected to prompt the user for more + * information, or fail if it prefers. + * @return {string} + * @constructor + */ + static get PROMPT() { return "PROMPT"; } + + /** + * The auto discovery was successful. + * @return {string} + * @constructor + */ + static get SUCCESS() { return "SUCCESS"; } + + /** + * Attempts to automatically discover client configuration information + * prior to logging in. Such information includes the homeserver URL + * and identity server URL the client would want. Additional details + * may also be discovered, and will be transparently included in the + * response object unaltered. + * @param {string} domain The homeserver domain to perform discovery + * on. For example, "matrix.org". + * @return {Promise} Resolves to the discovered + * configuration, which may include error states. Rejects on unexpected + * failure, not when discovery fails. + */ + static async findClientConfig(domain) { + if (!domain || typeof(domain) !== "string" || domain.length === 0) { + throw new Error("'domain' must be a string of non-zero length"); + } + + // We use a .well-known lookup for all cases. According to the spec, we + // can do other discovery mechanisms if we want such as custom lookups + // however we won't bother with that here (mostly because the spec only + // supports .well-known right now). + // + // By using .well-known, we need to ensure we at least pull out a URL + // for the homeserver. We don't really need an identity server configuration + // but will return one anyways (with state PROMPT) to make development + // easier for clients. If we can't get a homeserver URL, all bets are + // off on the rest of the config and we'll assume it is invalid too. + + // We default to an error state to make the first few checks easier to + // write. We'll update the properties of this object over the duration + // of this function. + const clientConfig = { + "m.homeserver": { + state: AutoDiscovery.FAIL_ERROR, + error: "Invalid homeserver discovery response", + base_url: null, + }, + "m.identity_server": { + // Technically, we don't have a problem with the identity server + // config at this point. + state: AutoDiscovery.PROMPT, + error: null, + base_url: null, + }, + }; + + // Step 1: Actually request the .well-known JSON file and make sure it + // at least has a homeserver definition. + const wellknown = await this._fetchWellKnownObject( + `https://${domain}/.well-known/matrix/client`, + ); + if (!wellknown || wellknown.action !== "SUCCESS" + || !wellknown.raw["m.homeserver"] + || !wellknown.raw["m.homeserver"]["base_url"]) { + logger.error("No m.homeserver key in well-known response"); + if (wellknown.reason) logger.error(wellknown.reason); + if (wellknown.action === "IGNORE") { + clientConfig["m.homeserver"] = { + state: AutoDiscovery.PROMPT, + error: null, + base_url: null, + }; + } else { + // this can only ever be FAIL_PROMPT at this point. + clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT; + } + return Promise.resolve(clientConfig); + } + + // Step 2: Make sure the homeserver URL is valid *looking*. We'll make + // sure it points to a homeserver in Step 3. + const hsUrl = this._sanitizeWellKnownUrl( + wellknown.raw["m.homeserver"]["base_url"], + ); + if (!hsUrl) { + logger.error("Invalid base_url for m.homeserver"); + return Promise.resolve(clientConfig); + } + + // Step 3: Make sure the homeserver URL points to a homeserver. + const hsVersions = await this._fetchWellKnownObject( + `${hsUrl}/_matrix/client/versions`, + ); + if (!hsVersions || !hsVersions.raw["versions"]) { + logger.error("Invalid /versions response"); + return Promise.resolve(clientConfig); + } + + // Step 4: Now that the homeserver looks valid, update our client config. + clientConfig["m.homeserver"] = { + state: AutoDiscovery.SUCCESS, + error: null, + base_url: hsUrl, + }; + + // Step 5: Try to pull out the identity server configuration + let isUrl = ""; + if (wellknown.raw["m.identity_server"]) { + // We prepare a failing identity server response to save lines later + // in this branch. Note that we also fail the homeserver check in the + // object because according to the spec we're supposed to FAIL_ERROR + // if *anything* goes wrong with the IS validation, including invalid + // format. This means we're supposed to stop discovery completely. + const failingClientConfig = { + "m.homeserver": { + state: AutoDiscovery.FAIL_ERROR, + error: "Invalid identity server discovery response", + + // We'll provide the base_url that was previously valid for + // debugging purposes. + base_url: clientConfig["m.homeserver"].base_url, + }, + "m.identity_server": { + state: AutoDiscovery.FAIL_ERROR, + error: "Invalid identity server discovery response", + base_url: null, + }, + }; + + // Step 5a: Make sure the URL is valid *looking*. We'll make sure it + // points to an identity server in Step 5b. + isUrl = this._sanitizeWellKnownUrl( + wellknown.raw["m.identity_server"]["base_url"], + ); + if (!isUrl) { + logger.error("Invalid base_url for m.identity_server"); + return Promise.resolve(failingClientConfig); + } + + // Step 5b: Verify there is an identity server listening on the provided + // URL. + const isResponse = await this._fetchWellKnownObject( + `${isUrl}/_matrix/identity/api/v1`, + ); + if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") { + logger.error("Invalid /api/v1 response"); + return Promise.resolve(failingClientConfig); + } + } + + // Step 6: Now that the identity server is valid, or never existed, + // populate the IS section. + if (isUrl && isUrl.length > 0) { + clientConfig["m.identity_server"] = { + state: AutoDiscovery.SUCCESS, + error: null, + base_url: isUrl, + }; + } + + // Step 7: Copy any other keys directly into the clientConfig. This is for + // things like custom configuration of services. + Object.keys(wellknown.raw) + .filter((k) => k !== "m.homeserver" && k !== "m.identity_server") + .map((k) => clientConfig[k] = wellknown.raw[k]); + + // Step 8: Give the config to the caller (finally) + return Promise.resolve(clientConfig); + } + + /** + * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and + * is suitable for the requirements laid out by .well-known auto discovery. + * If valid, the URL will also be stripped of any trailing slashes. + * @param {string} url The potentially invalid URL to sanitize. + * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid. + * @private + */ + static _sanitizeWellKnownUrl(url) { + if (!url) return false; + + try { + // We have to try and parse the URL using the NodeJS URL + // library if we're on NodeJS and use the browser's URL + // library when we're in a browser. To accomplish this, we + // try the NodeJS version first and fall back to the browser. + let parsed = null; + try { + if (NodeURL) parsed = new NodeURL(url); + else parsed = new URL(url); + } catch (e) { + parsed = new URL(url); + } + + if (!parsed || !parsed.hostname) return false; + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false; + + const port = parsed.port ? `:${parsed.port}` : ""; + const path = parsed.pathname ? parsed.pathname : ""; + let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`; + if (saferUrl.endsWith("/")) { + saferUrl = saferUrl.substring(0, saferUrl.length - 1); + } + return saferUrl; + } catch (e) { + logger.error(e); + return false; + } + } + + /** + * Fetches a JSON object from a given URL, as expected by all .well-known + * related lookups. If the server gives a 404 then the `action` will be + * IGNORE. If the server returns something that isn't JSON, the `action` + * will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT. + * + * The returned object will be a result of the call in object form with + * the following properties: + * raw: The JSON object returned by the server. + * action: One of SUCCESS, IGNORE, or FAIL_PROMPT. + * reason: Relatively human readable description of what went wrong. + * error: The actual Error, if one exists. + * @param {string} url The URL to fetch a JSON object from. + * @return {Promise} Resolves to the returned state. + * @private + */ + static async _fetchWellKnownObject(url) { + return new Promise(function(resolve, reject) { + const request = require("./matrix").getRequest(); + if (!request) throw new Error("No request library available"); + request( + { method: "GET", uri: url }, + (err, response, body) => { + if (err || response.statusCode < 200 || response.statusCode >= 300) { + let action = "FAIL_PROMPT"; + let reason = (err ? err.message : null) || "General failure"; + if (response.statusCode === 404) { + action = "IGNORE"; + reason = "No .well-known JSON file found"; + } + resolve({raw: {}, action: action, reason: reason, error: err}); + return; + } + + try { + resolve({raw: JSON.parse(body), action: "SUCCESS"}); + } catch (e) { + let reason = "General failure"; + if (e.name === "SyntaxError") reason = "Invalid JSON"; + resolve({ + raw: {}, + action: "FAIL_PROMPT", + reason: reason, error: e, + }); + } + }, + ); + }); + } +} diff --git a/src/base-apis.js b/src/base-apis.js index bf2381790..4e9fed77b 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -262,7 +262,19 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) { utils.extend(login_data, data); return this._http.authedRequest( - callback, "POST", "/login", undefined, login_data, + (error, response) => { + if (loginType === "m.login.password" && response && + response.access_token && response.user_id) { + this._http.opts.accessToken = response.access_token; + this.credentials = { + userId: response.user_id, + }; + } + + if (callback) { + callback(error, response); + } + }, "POST", "/login", undefined, login_data, ); }; @@ -298,9 +310,23 @@ MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) { * @return {string} The HS URL to hit to begin the CAS login process. */ MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) { - return this._http.getUrl("/login/cas/redirect", { + return this.getSsoLoginUrl(redirectUrl, "cas"); +}; + +/** + * @param {string} redirectUrl The URL to redirect to after the HS + * authenticates with the SSO. + * @param {string} loginType The type of SSO login we are doing (sso or cas). + * Defaults to 'sso'. + * @return {string} The HS URL to hit to begin the SSO login process. + */ +MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) { + if (loginType === undefined) { + loginType = "sso"; + } + return this._http.getUrl("/login/"+loginType+"/redirect", { "redirectUrl": redirectUrl, - }, httpApi.PREFIX_UNSTABLE); + }, httpApi.PREFIX_R0); }; /** @@ -417,6 +443,69 @@ MatrixBaseApis.prototype.roomState = function(roomId, callback) { return this._http.authedRequest(callback, "GET", path); }; +/** + * Get an event in a room by its event id. + * @param {string} roomId + * @param {string} eventId + * @param {module:client.callback} callback Optional. + * + * @return {Promise} Resolves to an object containing the event. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.fetchRoomEvent = function(roomId, eventId, callback) { + const path = utils.encodeUri( + "/rooms/$roomId/event/$eventId", { + $roomId: roomId, + $eventId: eventId, + }, + ); + return this._http.authedRequest(callback, "GET", path); +}; + +/** + * @param {string} roomId + * @param {string} includeMembership the membership type to include in the response + * @param {string} excludeMembership the membership type to exclude from the response + * @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: dictionary of userid to profile information + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.members = +function(roomId, includeMembership, excludeMembership, atEventId, callback) { + const queryParams = {}; + if (includeMembership) { + queryParams.membership = includeMembership; + } + if (excludeMembership) { + queryParams.not_membership = excludeMembership; + } + if (atEventId) { + queryParams.at = atEventId; + } + + const queryString = utils.encodeParams(queryParams); + + const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, + {$roomId: roomId}); + return this._http.authedRequest(callback, "GET", path); +}; + +/** + * Upgrades a room to a new protocol version + * @param {string} roomId + * @param {string} newVersion The target version to upgrade to + * @return {module:client.Promise} Resolves: Object with key 'replacement_room' + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.upgradeRoom = function(roomId, newVersion) { + const path = utils.encodeUri("/rooms/$roomId/upgrade", {$roomId: roomId}); + return this._http.authedRequest( + undefined, "POST", path, undefined, {new_version: newVersion}, + ); +}; + + /** * @param {string} groupId * @return {module:client.Promise} Resolves: Group summary object @@ -864,6 +953,28 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest = ); }; +/** + * @return {module:client.Promise} Resolves: A list of the user's current rooms + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getJoinedRooms = function() { + const path = utils.encodeUri("/joined_rooms"); + return this._http.authedRequest(undefined, "GET", path); +}; + +/** + * Retrieve membership info. for a room. + * @param {string} roomId ID of the room to get membership for + * @return {module:client.Promise} Resolves: A list of currently joined users + * and their profile data. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) { + const path = utils.encodeUri("/rooms/$roomId/joined_members", { + $roomId: roomId, + }); + return this._http.authedRequest(undefined, "GET", path); +}; // Room Directory operations // ========================= @@ -1722,7 +1833,7 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() { * Get information on how a specific place on a third party protocol * may be reached. * @param {string} protocol The protocol given in getThirdpartyProtocols() - * @param {object} params Protocol-specific parameters, as given in th + * @param {object} params Protocol-specific parameters, as given in the * response to getThirdpartyProtocols() * @return {module:client.Promise} Resolves to the result object */ @@ -1737,6 +1848,25 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) { ); }; +/** + * Get information on how a specific user on a third party protocol + * may be reached. + * @param {string} protocol The protocol given in getThirdpartyProtocols() + * @param {object} params Protocol-specific parameters, as given in the + * response to getThirdpartyProtocols() + * @return {module:client.Promise} Resolves to the result object + */ +MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) { + const path = utils.encodeUri("/thirdparty/user/$protocol", { + $protocol: protocol, + }); + + return this._http.authedRequestWithPrefix( + undefined, "GET", path, params, undefined, + httpApi.PREFIX_UNSTABLE, + ); +}; + /** * MatrixBaseApis object */ diff --git a/src/client.js b/src/client.js index e8c44c653..c942acb9d 100644 --- a/src/client.js +++ b/src/client.js @@ -41,18 +41,46 @@ const SyncApi = require("./sync"); const MatrixBaseApis = require("./base-apis"); const MatrixError = httpApi.MatrixError; const ContentHelpers = require("./content-helpers"); +const olmlib = require("./crypto/olmlib"); import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; -const SCROLLBACK_DELAY_MS = 3000; -let CRYPTO_ENABLED = false; +import Crypto from './crypto'; +import { isCryptoAvailable } from './crypto'; +import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; +import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; +import { randomString } from './randomstring'; -try { - var Crypto = require("./crypto"); - CRYPTO_ENABLED = true; -} catch (e) { - console.warn("Unable to load crypto module: crypto will be disabled: " + e); +// Disable warnings for now: we use deprecated bluebird functions +// and need to migrate, but they spam the console with warnings. +Promise.config({warnings: false}); + + +const SCROLLBACK_DELAY_MS = 3000; +const CRYPTO_ENABLED = isCryptoAvailable(); + +function keysFromRecoverySession(sessions, decryptionKey, roomId) { + const keys = []; + for (const [sessionId, sessionData] of Object.entries(sessions)) { + try { + const decrypted = keyFromRecoverySession(sessionData, decryptionKey); + decrypted.session_id = sessionId; + decrypted.room_id = roomId; + keys.push(decrypted); + } catch (e) { + console.log("Failed to decrypt session from backup"); + } + } + return keys; +} + +function keyFromRecoverySession(session, decryptionKey) { + return JSON.parse(decryptionKey.decrypt( + session.session_data.ephemeral, + session.session_data.mac, + session.session_data.ciphertext, + )); } /** @@ -128,6 +156,8 @@ function MatrixClient(opts) { MatrixBaseApis.call(this, opts); + this.olmVersion = null; // Populated after initCrypto is done + this.reEmitter = new ReEmitter(this); this.store = opts.store || new StubStore(); @@ -180,10 +210,6 @@ function MatrixClient(opts) { this._forceTURN = opts.forceTURN || false; - if (CRYPTO_ENABLED) { - this.olmVersion = Crypto.getOlmVersion(); - } - // List of which rooms have encryption enabled: separate from crypto because // we still want to know which rooms are encrypted even if crypto is disabled: // we don't want to start sending unencrypted events to them. @@ -191,6 +217,8 @@ function MatrixClient(opts) { // The pushprocessor caches useful things, so keep one and re-use it this._pushProcessor = new PushProcessor(this); + + this._serverSupportsLazyLoading = null; } utils.inherits(MatrixClient, EventEmitter); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); @@ -287,6 +315,21 @@ MatrixClient.prototype.getSyncState = function() { return this._syncApi.getSyncState(); }; +/** + * Returns the additional data object associated with + * the current sync state, or null if there is no + * such data. + * Sync errors, if available, are put in the 'error' key of + * this object. + * @return {?Object} + */ +MatrixClient.prototype.getSyncStateData = function() { + if (!this._syncApi) { + return null; + } + return this._syncApi.getSyncStateData(); +}; + /** * Return whether the client is configured for a guest account. * @return {boolean} True if this is a guest access_token (or no token is supplied). @@ -356,6 +399,13 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) { * successfully initialised. */ MatrixClient.prototype.initCrypto = async function() { + if (!isCryptoAvailable()) { + throw new Error( + `End-to-end encryption not supported in this js-sdk build: did ` + + `you remember to load the olm library?`, + ); + } + if (this._crypto) { console.warn("Attempt to re-initialise e2e encryption on MatrixClient"); return; @@ -373,13 +423,6 @@ MatrixClient.prototype.initCrypto = async function() { // initialise the list of encrypted rooms (whether or not crypto is enabled) await this._roomList.init(); - if (!CRYPTO_ENABLED) { - throw new Error( - `End-to-end encryption not supported in this js-sdk build: did ` + - `you remember to load the olm library?`, - ); - } - const userId = this.getUserId(); if (userId === null) { throw new Error( @@ -411,6 +454,9 @@ MatrixClient.prototype.initCrypto = async function() { await crypto.init(); + this.olmVersion = Crypto.getOlmVersion(); + + // if crypto initialisation was successful, tell it to attach its event // handlers. crypto.registerEventHandlers(this); @@ -514,7 +560,15 @@ MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified) if (verified === undefined) { verified = true; } - return _setDeviceVerification(this, userId, deviceId, verified, null); + const prom = _setDeviceVerification(this, userId, deviceId, verified, null); + + // if one of the user's own devices is being marked as verified / unverified, + // check the key backup status, since whether or not we use this depends on + // whether it has a signature from a verified device + if (userId == this.credentials.userId) { + this._crypto.checkKeyBackup(); + } + return prom; }; /** @@ -673,6 +727,21 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) { return this._roomList.isRoomEncrypted(roomId); }; +/** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * @param {string} roomId The ID of the room to discard the session for + * + * This should not normally be necessary. + */ +MatrixClient.prototype.forceDiscardSession = function(roomId) { + if (!this._crypto) { + throw new Error("End-to-End encryption disabled"); + } + this._crypto.forceDiscardSession(roomId); +}; + /** * Get a list containing all of the room keys * @@ -703,6 +772,333 @@ MatrixClient.prototype.importRoomKeys = function(keys) { return this._crypto.importRoomKeys(keys); }; +/** + * Get information about the current key backup. + * @returns {Promise} Information object from API or null + */ +MatrixClient.prototype.getKeyBackupVersion = function() { + return this._http.authedRequest( + undefined, "GET", "/room_keys/version", + ).then((res) => { + if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) { + const err = "Unknown backup algorithm: " + res.algorithm; + return Promise.reject(err); + } else if (!(typeof res.auth_data === "object") + || !res.auth_data.public_key) { + const err = "Invalid backup data returned"; + return Promise.reject(err); + } else { + return res; + } + }).catch((e) => { + if (e.errcode === 'M_NOT_FOUND') { + return null; + } else { + throw e; + } + }); +}; + +/** + * @param {object} info key backup info dict from getKeyBackupVersion() + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool], + * device: [DeviceInfo], + * ] + * } + */ +MatrixClient.prototype.isKeyBackupTrusted = function(info) { + return this._crypto.isKeyBackupTrusted(info); +}; + +/** + * @returns {bool} true if the client is configured to back up keys to + * the server, otherwise false. + */ +MatrixClient.prototype.getKeyBackupEnabled = function() { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + return Boolean(this._crypto.backupKey); +}; + +/** + * Enable backing up of keys, using data previously returned from + * getKeyBackupVersion. + * + * @param {object} info Backup information object as returned by getKeyBackupVersion + */ +MatrixClient.prototype.enableKeyBackup = function(info) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + this._crypto.backupInfo = info; + if (this._crypto.backupKey) this._crypto.backupKey.free(); + this._crypto.backupKey = new global.Olm.PkEncryption(); + this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); + + this.emit('crypto.keyBackupStatus', true); +}; + +/** + * Disable backing up of keys. + */ +MatrixClient.prototype.disableKeyBackup = function() { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + this._crypto.backupInfo = null; + if (this._crypto.backupKey) this._crypto.backupKey.free(); + this._crypto.backupKey = null; + + this.emit('crypto.keyBackupStatus', false); +}; + +/** + * Set up the data required to create a new backup version. The backup version + * will not be created and enabled until createKeyBackupVersion is called. + * + * @param {string} password Passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * Optional. + * + * @returns {Promise} Object that can be passed to createKeyBackupVersion and + * additionally has a 'recovery_key' member with the user-facing recovery key string. + */ +MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const decryption = new global.Olm.PkDecryption(); + try { + let publicKey; + const authData = {}; + if (password) { + const keyInfo = await keyForNewBackup(password); + publicKey = decryption.init_with_private_key(keyInfo.key); + authData.private_key_salt = keyInfo.salt; + authData.private_key_iterations = keyInfo.iterations; + } else { + publicKey = decryption.generate_key(); + } + + authData.public_key = publicKey; + + return { + algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, + auth_data: authData, + recovery_key: encodeRecoveryKey(decryption.get_private_key()), + }; + } finally { + decryption.free(); + } +}; + +/** + * Create a new key backup version and enable it, using the information return + * from prepareKeyBackupVersion. + * + * @param {object} info Info object from prepareKeyBackupVersion + * @returns {Promise} Object with 'version' param indicating the version created + */ +MatrixClient.prototype.createKeyBackupVersion = function(info) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const data = { + algorithm: info.algorithm, + auth_data: info.auth_data, + }; + return this._crypto._signObject(data.auth_data).then(() => { + return this._http.authedRequest( + undefined, "POST", "/room_keys/version", undefined, data, + ); + }).then((res) => { + this.enableKeyBackup({ + algorithm: info.algorithm, + auth_data: info.auth_data, + version: res.version, + }); + return res; + }); +}; + +MatrixClient.prototype.deleteKeyBackupVersion = function(version) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeyBackupVersion + // so this is symmetrical). + if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) { + this.disableKeyBackup(); + } + + const path = utils.encodeUri("/room_keys/version/$version", { + $version: version, + }); + + return this._http.authedRequest( + undefined, "DELETE", path, undefined, undefined, + ); +}; + +MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) { + let path; + if (sessionId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { + $roomId: roomId, + $sessionId: sessionId, + }); + } else if (roomId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId", { + $roomId: roomId, + }); + } else { + path = "/room_keys/keys"; + } + const queryData = version === undefined ? undefined : { version: version }; + return { + path: path, + queryData: queryData, + }; +}; + +/** + * Back up session keys to the homeserver. + * @param {string} roomId ID of the room that the keys are for Optional. + * @param {string} sessionId ID of the session that the keys are for Optional. + * @param {integer} version backup version Optional. + * @param {object} data Object keys to send + * @return {module:client.Promise} a promise that will resolve when the keys + * are uploaded + */ +MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + return this._http.authedRequest( + undefined, "PUT", path.path, path.queryData, data, + ); +}; + +MatrixClient.prototype.backupAllGroupSessions = function(version) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.backupAllGroupSessions(version); +}; + +MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { + try { + decodeRecoveryKey(recoveryKey); + return true; + } catch (e) { + return false; + } +}; + +MatrixClient.prototype.restoreKeyBackupWithPassword = async function( + password, targetRoomId, targetSessionId, version, +) { + const backupInfo = await this.getKeyBackupVersion(); + + const privKey = await keyForExistingBackup(backupInfo, password); + return this._restoreKeyBackup( + privKey, targetRoomId, targetSessionId, version, + ); +}; + +MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( + recoveryKey, targetRoomId, targetSessionId, version, +) { + const privKey = decodeRecoveryKey(recoveryKey); + return this._restoreKeyBackup( + privKey, targetRoomId, targetSessionId, version, + ); +}; + +MatrixClient.prototype._restoreKeyBackup = function( + privKey, targetRoomId, targetSessionId, version, +) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + let totalKeyCount = 0; + let keys = []; + + const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); + + const decryption = new global.Olm.PkDecryption(); + try { + decryption.init_with_private_key(privKey); + } catch(e) { + decryption.free(); + throw e; + } + + return this._http.authedRequest( + undefined, "GET", path.path, path.queryData, + ).then((res) => { + if (res.rooms) { + for (const [roomId, roomData] of Object.entries(res.rooms)) { + if (!roomData.sessions) continue; + + totalKeyCount += Object.keys(roomData.sessions).length; + const roomKeys = keysFromRecoverySession( + roomData.sessions, decryption, roomId, roomKeys, + ); + for (const k of roomKeys) { + k.room_id = roomId; + keys.push(k); + } + } + } else if (res.sessions) { + totalKeyCount = Object.keys(res.sessions).length; + keys = keysFromRecoverySession( + res.sessions, decryption, targetRoomId, keys, + ); + } else { + totalKeyCount = 1; + try { + const key = keyFromRecoverySession(res, decryption); + key.room_id = targetRoomId; + key.session_id = targetSessionId; + keys.push(key); + } catch (e) { + console.log("Failed to decrypt session from backup"); + } + } + + return this.importRoomKeys(keys); + }).then(() => { + return {total: totalKeyCount, imported: keys.length}; + }).finally(() => { + decryption.free(); + }); +}; + +MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + return this._http.authedRequest( + undefined, "DELETE", path.path, path.queryData, + ); +}; + // Group ops // ========= // Operations on groups that come down the sync stream (ie. ones the @@ -727,6 +1123,17 @@ MatrixClient.prototype.getGroups = function() { return this.store.getGroups(); }; +/** + * Get the config for the media repository. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves with an object containing the config. + */ +MatrixClient.prototype.getMediaConfig = function(callback) { + return this._http.authedRequestWithPrefix( + callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0, + ); +}; + // Room ops // ======== @@ -750,6 +1157,37 @@ MatrixClient.prototype.getRooms = function() { return this.store.getRooms(); }; +/** + * Retrieve all rooms that should be displayed to the user + * This is essentially getRooms() with some rooms filtered out, eg. old versions + * of rooms that have been replaced or (in future) other rooms that have been + * marked at the protocol level as not to be displayed to the user. + * @return {Room[]} A list of rooms, or an empty list if there is no data store. + */ +MatrixClient.prototype.getVisibleRooms = function() { + const allRooms = this.store.getRooms(); + + const replacedRooms = new Set(); + for (const r of allRooms) { + const createEvent = r.currentState.getStateEvents('m.room.create', ''); + // invites are included in this list and we don't know their create events yet + if (createEvent) { + const predecessor = createEvent.getContent()['predecessor']; + if (predecessor && predecessor['room_id']) { + replacedRooms.add(predecessor['room_id']); + } + } + } + + return allRooms.filter((r) => { + const tombstone = r.currentState.getStateEvents('m.room.tombstone', ''); + if (tombstone && replacedRooms.has(r.roomId)) { + return false; + } + return true; + }); +}; + /** * Retrieve a user. * @param {string} userId The user ID to retrieve. @@ -842,6 +1280,8 @@ MatrixClient.prototype.isUserIgnored = function(userId) { * Default: true. * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, * the signing URL is passed in this parameter. + * @param {string[]} opts.viaServers The server names to try and join through in + * addition to those that are automatically chosen. * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: Room object. * @return {module:http-api.MatrixError} Rejects: with an error response. @@ -870,6 +1310,13 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { ); } + const queryString = {}; + if (opts.viaServers) { + queryString["server_name"] = opts.viaServers; + } + + const reqOpts = {qsStringifyOptions: {arrayFormat: 'repeat'}}; + const defer = Promise.defer(); const self = this; @@ -880,7 +1327,8 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { } const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias}); - return self._http.authedRequest(undefined, "POST", path, undefined, data); + return self._http.authedRequest( + undefined, "POST", path, queryString, data, reqOpts); }).then(function(res) { const roomId = res.room_id; const syncApi = new SyncApi(self, self._clientOpts); @@ -1100,6 +1548,13 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId, room.addPendingEvent(localEvent, txnId); } + // addPendingEvent can change the state to NOT_SENT if it believes + // that there's other events that have failed. We won't bother to + // try sending the event if the state has changed as such. + if (localEvent.status === EventStatus.NOT_SENT) { + return Promise.reject(new Error("Event blocked by other events not yet sent")); + } + return _sendEvent(this, room, localEvent, callback); }; @@ -1807,6 +2262,27 @@ MatrixClient.prototype.mxcUrlToHttp = ); }; +/** + * Sets a new status message for the user. The message may be null/falsey + * to clear the message. + * @param {string} newMessage The new message to set. + * @return {module:client.Promise} Resolves: to nothing + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) { + return Promise.all(this.getRooms().map((room) => { + const isJoined = room.getMyMembership() === "join"; + const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2; + if (isJoined && looksLikeDm) { + return this.sendStateEvent(room.roomId, "im.vector.user_status", { + status: newMessage, + }, this.getUserId()); + } else { + return Promise.resolve(); + } + })); +}; + /** * @param {Object} opts Options to apply * @param {string} opts.presence One of "online", "offline" or "unavailable" @@ -1919,14 +2395,6 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) { // reduce the required number of events appropriately limit = limit - numAdded; - const path = utils.encodeUri( - "/rooms/$roomId/messages", {$roomId: room.roomId}, - ); - const params = { - from: room.oldState.paginationToken, - limit: limit, - dir: 'b', - }; const defer = Promise.defer(); info = { promise: defer.promise, @@ -1936,9 +2404,17 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) { // wait for a time before doing this request // (which may be 0 in order not to special case the code paths) Promise.delay(timeToWaitMs).then(function() { - return self._http.authedRequest(callback, "GET", path, params); + return self._createMessagesRequest( + room.roomId, + room.oldState.paginationToken, + limit, + 'b'); }).done(function(res) { const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self)); + if (res.state) { + const stateEvents = utils.map(res.state, _PojoToMatrixEventMapper(self)); + room.currentState.setUnknownStateEvents(stateEvents); + } room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline()); room.oldState.paginationToken = res.end; if (res.chunk.length === 0) { @@ -1957,73 +2433,6 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) { return defer.promise; }; -/** - * Take an EventContext, and back/forward-fill results. - * - * @param {module:models/event-context.EventContext} eventContext context - * object to be updated - * @param {Object} opts - * @param {boolean} opts.backwards true to fill backwards, false to go forwards - * @param {boolean} opts.limit number of events to request - * - * @return {module:client.Promise} Resolves: updated EventContext object - * @return {Error} Rejects: with an error response. - */ -MatrixClient.prototype.paginateEventContext = function(eventContext, opts) { - // TODO: we should implement a backoff (as per scrollback()) to deal more - // nicely with HTTP errors. - opts = opts || {}; - const backwards = opts.backwards || false; - - const token = eventContext.getPaginateToken(backwards); - if (!token) { - // no more results. - return Promise.reject(new Error("No paginate token")); - } - - const dir = backwards ? 'b' : 'f'; - const pendingRequest = eventContext._paginateRequests[dir]; - - if (pendingRequest) { - // already a request in progress - return the existing promise - return pendingRequest; - } - - const path = utils.encodeUri( - "/rooms/$roomId/messages", {$roomId: eventContext.getEvent().getRoomId()}, - ); - const params = { - from: token, - limit: ('limit' in opts) ? opts.limit : 30, - dir: dir, - }; - - const self = this; - const promise = - self._http.authedRequest(undefined, "GET", path, params, - ).then(function(res) { - let token = res.end; - if (res.chunk.length === 0) { - token = null; - } else { - const matrixEvents = utils.map(res.chunk, self.getEventMapper()); - if (backwards) { - // eventContext expects the events in timeline order, but - // back-pagination returns them in reverse order. - matrixEvents.reverse(); - } - eventContext.addEvents(matrixEvents, backwards); - } - eventContext.setPaginateToken(token, backwards); - return eventContext; - }).finally(function() { - eventContext._paginateRequests[dir] = null; - }); - eventContext._paginateRequests[dir] = promise; - - return promise; -}; - /** * Get an EventTimeline for the given event * @@ -2057,11 +2466,16 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { }, ); + let params = undefined; + if (this._clientOpts.lazyLoadMembers) { + params = {filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)}; + } + // TODO: we should implement a backoff (as per scrollback()) to deal more // nicely with HTTP errors. const self = this; const promise = - self._http.authedRequest(undefined, "GET", path, + self._http.authedRequest(undefined, "GET", path, params, ).then(function(res) { if (!res.event) { throw new Error("'event' not in '/context' result - homeserver too old?"); @@ -2088,6 +2502,9 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { timeline.initialiseState(utils.map(res.state, self.getEventMapper())); timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end; + } else { + const stateEvents = utils.map(res.state, self.getEventMapper()); + timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents); } timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start); @@ -2102,6 +2519,49 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { return promise; }; +/** + * Makes a request to /messages with the appropriate lazy loading filter set. + * XXX: if we do get rid of scrollback (as it's not used at the moment), + * we could inline this method again in paginateEventTimeline as that would + * then be the only call-site + * @param {string} roomId + * @param {string} fromToken + * @param {number} limit the maximum amount of events the retrieve + * @param {string} dir 'f' or 'b' + * @param {Filter} timelineFilter the timeline filter to pass + * @return {Promise} + */ +MatrixClient.prototype._createMessagesRequest = +function(roomId, fromToken, limit, dir, timelineFilter = undefined) { + const path = utils.encodeUri( + "/rooms/$roomId/messages", {$roomId: roomId}, + ); + if (limit === undefined) { + limit = 30; + } + const params = { + from: fromToken, + limit: limit, + dir: dir, + }; + + let filter = null; + if (this._clientOpts.lazyLoadMembers) { + // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, + // so the timelineFilter doesn't get written into it below + filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER); + } + if (timelineFilter) { + // XXX: it's horrific that /messages' filter parameter doesn't match + // /sync's one - see https://matrix.org/jira/browse/SPEC-451 + filter = filter || {}; + Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()); + } + if (filter) { + params.filter = JSON.stringify(filter); + } + return this._http.authedRequest(undefined, "GET", path, params); +}; /** * Take an EventTimeline, and back/forward-fill results. @@ -2196,25 +2656,18 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) { throw new Error("Unknown room " + eventTimeline.getRoomId()); } - path = utils.encodeUri( - "/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()}, - ); - params = { - from: token, - limit: ('limit' in opts) ? opts.limit : 30, - dir: dir, - }; - - const filter = eventTimeline.getFilter(); - if (filter) { - // XXX: it's horrific that /messages' filter parameter doesn't match - // /sync's one - see https://matrix.org/jira/browse/SPEC-451 - params.filter = JSON.stringify(filter.getRoomTimelineFilterComponent()); - } - - promise = - this._http.authedRequest(undefined, "GET", path, params, - ).then(function(res) { + promise = this._createMessagesRequest( + eventTimeline.getRoomId(), + token, + opts.limit, + dir, + eventTimeline.getFilter()); + promise.then(function(res) { + if (res.state) { + const roomState = eventTimeline.getState(dir); + const stateEvents = utils.map(res.state, self.getEventMapper()); + roomState.setUnknownStateEvents(stateEvents); + } const token = res.end; const matrixEvents = utils.map(res.chunk, self.getEventMapper()); eventTimeline.getTimelineSet() @@ -3019,8 +3472,11 @@ MatrixClient.prototype.getTurnServers = function() { * * @param {Boolean=} opts.disablePresence True to perform syncing without automatically * updating presence. + * @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during + * initial sync but fetch them when needed by calling `loadOutOfBandMembers` + * This will override the filter option at this moment. */ -MatrixClient.prototype.startClient = function(opts) { +MatrixClient.prototype.startClient = async function(opts) { if (this.clientRunning) { // client is already running. return; @@ -3058,11 +3514,29 @@ MatrixClient.prototype.startClient = function(opts) { return this._canResetTimelineCallback(roomId); }; this._clientOpts = opts; - this._syncApi = new SyncApi(this, opts); this._syncApi.sync(); }; +/** + * store client options with boolean/string/numeric values + * to know in the next session what flags the sync data was + * created with (e.g. lazy loading) + * @param {object} opts the complete set of client options + * @return {Promise} for store operation */ +MatrixClient.prototype._storeClientOptions = function() { + const primTypes = ["boolean", "string", "number"]; + const serializableOpts = Object.entries(this._clientOpts) + .filter(([key, value]) => { + return primTypes.includes(typeof value); + }) + .reduce((obj, [key, value]) => { + obj[key] = value; + return obj; + }, {}); + return this.store.storeClientOptions(serializableOpts); +}; + /** * High level helper method to stop the client from polling and allow a * clean shutdown. @@ -3085,6 +3559,36 @@ MatrixClient.prototype.stopClient = function() { global.clearTimeout(this._checkTurnServersTimeoutID); }; +/* + * Query the server to see if it support members lazy loading + * @return {Promise} true if server supports lazy loading + */ +MatrixClient.prototype.doesServerSupportLazyLoading = async function() { + if (this._serverSupportsLazyLoading === null) { + const response = await this._http.request( + undefined, // callback + "GET", "/_matrix/client/versions", + undefined, // queryParams + undefined, // data + { + prefix: '', + }, + ); + const unstableFeatures = response["unstable_features"]; + this._serverSupportsLazyLoading = + unstableFeatures && unstableFeatures["m.lazy_load_members"]; + } + return this._serverSupportsLazyLoading; +}; + +/* + * Get if lazy loading members is being used. + * @return {boolean} Whether or not members are lazy loaded by this client + */ +MatrixClient.prototype.hasLazyLoadMembersEnabled = function() { + return !!this._clientOpts.lazyLoadMembers; +}; + /* * Set a function which is called when /sync returns a 'limited' response. * It is called with a room ID and returns a boolean. It should return 'true' if the SDK @@ -3380,14 +3884,7 @@ MatrixClient.prototype.getEventMapper = function() { * @return {string} A new client secret */ MatrixClient.prototype.generateClientSecret = function() { - let ret = ""; - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (let i = 0; i < 32; i++) { - ret += chars.charAt(Math.floor(Math.random() * chars.length)); - } - - return ret; + return randomString(32); }; /** */ @@ -3430,6 +3927,12 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * a state of SYNCING. This is the equivalent of "syncComplete" in the * previous API. * + *
  • CATCHUP: The client has detected the connection to the server might be + * available again and will now try to do a sync again. As this sync might take + * a long time (depending how long ago was last synced, and general server + * performance) the client is put in this mode so the UI can reflect trying + * to catch up with the server after losing connection.
  • + * *
  • SYNCING : The client is currently polling for new events from the server. * This will be called after processing latest events from a sync.
  • * @@ -3453,11 +3956,11 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * +---->STOPPED * | * +----->PREPARED -------> SYNCING <--+ - * | ^ | ^ | - * | | | | | - * | | V | | - * null ------+ | +--------RECONNECTING | - * | | V | + * | ^ | ^ | + * | CATCHUP ----------+ | | | + * | ^ V | | + * null ------+ | +------- RECONNECTING | + * | V V | * +------->ERROR ---------------------+ * * NB: 'null' will never be emitted by this event. @@ -3507,7 +4010,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * * @param {?Object} data Data about this transition. * - * @param {MatrixError} data.err The matrix error if state=ERROR. + * @param {MatrixError} data.error The matrix error if state=ERROR. * * @param {String} data.oldSyncToken The 'since' token passed to /sync. * null for the first successful sync since this client was @@ -3625,6 +4128,24 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * }); */ +/** + * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() + * @event module:client~MatrixClient#"crypto.keyBackupStatus" + * @param {bool} enabled true if key backup has been enabled, otherwise false + * @example + * matrixClient.on("crypto.keyBackupStatus", function(enabled){ + * if (enabled) { + * [...] + * } + * }); + */ + +/** + * Fires when we want to suggest to the user that they restore their megolm keys + * from backup or by cross-signing the device. + * + * @event module:client~MatrixClient#"crypto.suggestKeyRestore" + */ // EventEmitter JSDocs diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index fa55f2fa6..751982b42 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -24,6 +24,7 @@ limitations under the License. import Promise from 'bluebird'; +import logger from '../logger'; import DeviceInfo from './deviceinfo'; import olmlib from './olmlib'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -71,6 +72,9 @@ export default class DeviceList { // } this._devices = {}; + // map of identity keys to the user who owns it + this._userByIdentityKey = {}; + // which users we are tracking device status for. // userId -> TRACKING_STATUS_* this._deviceTrackingStatus = {}; // loaded from storage in load() @@ -110,7 +114,7 @@ export default class DeviceList { 'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => { if (deviceData === null) { - console.log("Migrating e2e device data..."); + logger.log("Migrating e2e device data..."); this._devices = this._sessionStore.getAllEndToEndDevices() || {}; this._deviceTrackingStatus = ( this._sessionStore.getEndToEndDeviceTrackingStatus() || {} @@ -128,6 +132,16 @@ export default class DeviceList { deviceData.trackingStatus : {}; this._syncToken = deviceData ? deviceData.syncToken : null; } + this._userByIdentityKey = {}; + for (const user of Object.keys(this._devices)) { + const userDevices = this._devices[user]; + for (const device of Object.keys(userDevices)) { + const idKey = userDevices[device].keys['curve25519:'+device]; + if (idKey !== undefined) { + this._userByIdentityKey[idKey] = user; + } + } + } }); }, ); @@ -145,6 +159,12 @@ export default class DeviceList { } } + stop() { + if (this._saveTimer !== null) { + clearTimeout(this._saveTimer); + } + } + /** * Save the device tracking state to storage, if any changes are * pending other than updating the sync token @@ -190,7 +210,7 @@ export default class DeviceList { const resolveSavePromise = this._resolveSavePromise; this._savePromiseTime = targetTime; this._saveTimer = setTimeout(() => { - console.log('Saving device tracking data at token ' + this._syncToken); + logger.log('Saving device tracking data at token ' + this._syncToken); // null out savePromise now (after the delay but before the write), // otherwise we could return the existing promise when the save has // actually already happened. Likewise for the dirty flag. @@ -258,7 +278,7 @@ export default class DeviceList { if (this._keyDownloadsInProgressByUser[u]) { // already a key download in progress/queued for this user; its results // will be good enough for us. - console.log( + logger.log( `downloadKeys: already have a download in progress for ` + `${u}: awaiting its result`, ); @@ -269,13 +289,13 @@ export default class DeviceList { }); if (usersToDownload.length != 0) { - console.log("downloadKeys: downloading for", usersToDownload); + logger.log("downloadKeys: downloading for", usersToDownload); const downloadPromise = this._doKeyDownload(usersToDownload); promises.push(downloadPromise); } if (promises.length === 0) { - console.log("downloadKeys: already have all necessary keys"); + logger.log("downloadKeys: already have all necessary keys"); } return Promise.all(promises).then(() => { @@ -357,13 +377,17 @@ export default class DeviceList { /** * Find a device by curve25519 identity key * - * @param {string} userId owner of the device * @param {string} algorithm encryption algorithm * @param {string} senderKey curve25519 key to match * * @return {module:crypto/deviceinfo?} */ - getDeviceByIdentityKey(userId, algorithm, senderKey) { + getDeviceByIdentityKey(algorithm, senderKey) { + const userId = this._userByIdentityKey[senderKey]; + if (!userId) { + return null; + } + if ( algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM @@ -408,7 +432,23 @@ export default class DeviceList { * @param {Object} devs New device info for user */ storeDevicesForUser(u, devs) { + // remove previous devices from _userByIdentityKey + if (this._devices[u] !== undefined) { + for (const [deviceId, dev] of Object.entries(this._devices[u])) { + const identityKey = dev.keys['curve25519:'+deviceId]; + + delete this._userByIdentityKey[identityKey]; + } + } + this._devices[u] = devs; + + // add new ones + for (const [deviceId, dev] of Object.entries(devs)) { + const identityKey = dev.keys['curve25519:'+deviceId]; + + this._userByIdentityKey[identityKey] = u; + } this._dirty = true; } @@ -433,7 +473,7 @@ export default class DeviceList { throw new Error('userId must be a string; was '+userId); } if (!this._deviceTrackingStatus[userId]) { - console.log('Now tracking device list for ' + userId); + logger.log('Now tracking device list for ' + userId); this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; } // we don't yet persist the tracking status, since there may be a lot @@ -452,7 +492,7 @@ export default class DeviceList { */ stopTrackingDeviceList(userId) { if (this._deviceTrackingStatus[userId]) { - console.log('No longer tracking device list for ' + userId); + logger.log('No longer tracking device list for ' + userId); this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; // we don't yet persist the tracking status, since there may be a lot @@ -487,7 +527,7 @@ export default class DeviceList { */ invalidateUserDeviceList(userId) { if (this._deviceTrackingStatus[userId]) { - console.log("Marking device list outdated for", userId); + logger.log("Marking device list outdated for", userId); this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; // we don't yet persist the tracking status, since there may be a lot @@ -525,7 +565,23 @@ export default class DeviceList { * @param {Object} devices deviceId->{object} the new devices */ _setRawStoredDevicesForUser(userId, devices) { + // remove old devices from _userByIdentityKey + if (this._devices[userId] !== undefined) { + for (const [deviceId, dev] of Object.entries(this._devices[userId])) { + const identityKey = dev.keys['curve25519:'+deviceId]; + + delete this._userByIdentityKey[identityKey]; + } + } + this._devices[userId] = devices; + + // add new devices into _userByIdentityKey + for (const [deviceId, dev] of Object.entries(devices)) { + const identityKey = dev.keys['curve25519:'+deviceId]; + + this._userByIdentityKey[identityKey] = userId; + } } /** @@ -550,7 +606,7 @@ export default class DeviceList { ).then(() => { finished(true); }, (e) => { - console.error( + logger.error( 'Error downloading keys for ' + users + ":", e, ); finished(false); @@ -573,7 +629,7 @@ export default class DeviceList { // since we started this request. If that happens, we should // ignore the completion of the first one. if (this._keyDownloadsInProgressByUser[u] !== prom) { - console.log('Another update in the queue for', u, + logger.log('Another update in the queue for', u, '- not marking up-to-date'); return; } @@ -584,7 +640,7 @@ export default class DeviceList { // we didn't get any new invalidations since this download started: // this user's device list is now up to date. this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE; - console.log("Device list for", u, "now up to date"); + logger.log("Device list for", u, "now up to date"); } else { this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; } @@ -659,7 +715,7 @@ class DeviceListUpdateSerialiser { if (this._downloadInProgress) { // just queue up these users - console.log('Queued key download for', users); + logger.log('Queued key download for', users); return this._queuedQueryDeferred.promise; } @@ -679,7 +735,7 @@ class DeviceListUpdateSerialiser { const deferred = this._queuedQueryDeferred; this._queuedQueryDeferred = null; - console.log('Starting key download for', downloadUsers); + logger.log('Starting key download for', downloadUsers); this._downloadInProgress = true; const opts = {}; @@ -706,7 +762,7 @@ class DeviceListUpdateSerialiser { return prom; }).done(() => { - console.log('Completed key download for ' + downloadUsers); + logger.log('Completed key download for ' + downloadUsers); this._downloadInProgress = false; deferred.resolve(); @@ -716,7 +772,7 @@ class DeviceListUpdateSerialiser { this._doQueuedQueries(); } }, (e) => { - console.warn('Error downloading keys for ' + downloadUsers + ':', e); + logger.warn('Error downloading keys for ' + downloadUsers + ':', e); this._downloadInProgress = false; deferred.reject(e); }); @@ -725,7 +781,7 @@ class DeviceListUpdateSerialiser { } async _processQueryResponseForUser(userId, response) { - console.log('got keys for ' + userId + ':', response); + logger.log('got keys for ' + userId + ':', response); // map from deviceid -> deviceinfo for this user const userStore = {}; @@ -763,7 +819,7 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, } if (!(deviceId in userResult)) { - console.log("Device " + userId + ":" + deviceId + + logger.log("Device " + userId + ":" + deviceId + " has been removed"); delete userStore[deviceId]; updated = true; @@ -780,12 +836,12 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, // check that the user_id and device_id in the response object are // correct if (deviceResult.user_id !== userId) { - console.warn("Mismatched user_id " + deviceResult.user_id + + logger.warn("Mismatched user_id " + deviceResult.user_id + " in keys from " + userId + ":" + deviceId); continue; } if (deviceResult.device_id !== deviceId) { - console.warn("Mismatched device_id " + deviceResult.device_id + + logger.warn("Mismatched device_id " + deviceResult.device_id + " in keys from " + userId + ":" + deviceId); continue; } @@ -815,7 +871,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { const signKeyId = "ed25519:" + deviceId; const signKey = deviceResult.keys[signKeyId]; if (!signKey) { - console.warn("Device " + userId + ":" + deviceId + + logger.warn("Device " + userId + ":" + deviceId + " has no ed25519 key"); return false; } @@ -825,7 +881,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { try { await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); } catch (e) { - console.warn("Unable to verify signature on device " + + logger.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + e); return false; } @@ -842,7 +898,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { // best off sticking with the original keys. // // Should we warn the user about it somehow? - console.warn("Ed25519 key for device " + userId + ":" + + logger.warn("Ed25519 key for device " + userId + ":" + deviceId + " has changed"); return false; } diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index cda14779c..5868ca7bd 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -15,19 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ +import logger from '../logger'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; -/** - * olm.js wrapper - * - * @module crypto/OlmDevice - */ -const Olm = global.Olm; -if (!Olm) { - throw new Error("global.Olm is not defined"); -} - - // The maximum size of an event is 65K, and we base64 the content, so this is a // reasonable approximation to the biggest plaintext we can encrypt. const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; @@ -127,7 +117,7 @@ OlmDevice.prototype.init = async function() { await this._migrateFromSessionStore(); let e2eKeys; - const account = new Olm.Account(); + const account = new global.Olm.Account(); try { await _initialiseAccount( this._sessionStore, this._cryptoStore, this._pickleKey, account, @@ -161,7 +151,7 @@ async function _initialiseAccount(sessionStore, cryptoStore, pickleKey, account) * @return {array} The version of Olm. */ OlmDevice.getOlmVersion = function() { - return Olm.get_library_version(); + return global.Olm.get_library_version(); }; OlmDevice.prototype._migrateFromSessionStore = async function() { @@ -173,7 +163,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() { // Migrate from sessionStore pickledAccount = this._sessionStore.getEndToEndAccount(); if (pickledAccount !== null) { - console.log("Migrating account from session store"); + logger.log("Migrating account from session store"); this._cryptoStore.storeAccount(txn, pickledAccount); } } @@ -195,7 +185,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() { // has run against the same localstorage and created some spurious sessions. this._cryptoStore.countEndToEndSessions(txn, (count) => { if (count) { - console.log("Crypto store already has sessions: not migrating"); + logger.log("Crypto store already has sessions: not migrating"); return; } let numSessions = 0; @@ -207,7 +197,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() { ); } } - console.log( + logger.log( "Migrating " + numSessions + " sessions from session store", ); }); @@ -236,14 +226,14 @@ OlmDevice.prototype._migrateFromSessionStore = async function() { ), txn, ); } catch (e) { - console.warn( + logger.warn( "Failed to migrate session " + s.senderKey + "/" + s.sessionId + ": " + e.stack || e, ); } ++numIbSessions; } - console.log( + logger.log( "Migrated " + numIbSessions + " inbound group sessions from session store", ); @@ -268,7 +258,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() { */ OlmDevice.prototype._getAccount = function(txn, func) { this._cryptoStore.getAccount(txn, (pickledAccount) => { - const account = new Olm.Account(); + const account = new global.Olm.Account(); try { account.unpickle(this._pickleKey, pickledAccount); func(account); @@ -305,8 +295,8 @@ OlmDevice.prototype._storeAccount = function(txn, account) { */ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) { this._cryptoStore.getEndToEndSession( - deviceKey, sessionId, txn, (pickledSession) => { - this._unpickleSession(pickledSession, func); + deviceKey, sessionId, txn, (sessionInfo) => { + this._unpickleSession(sessionInfo, func); }, ); }; @@ -316,15 +306,17 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) { * function with it. The session object is destroyed once the function * returns. * - * @param {string} pickledSession + * @param {object} sessionInfo * @param {function} func * @private */ -OlmDevice.prototype._unpickleSession = function(pickledSession, func) { - const session = new Olm.Session(); +OlmDevice.prototype._unpickleSession = function(sessionInfo, func) { + const session = new global.Olm.Session(); try { - session.unpickle(this._pickleKey, pickledSession); - func(session); + session.unpickle(this._pickleKey, sessionInfo.session); + const unpickledSessInfo = Object.assign({}, sessionInfo, {session}); + + func(unpickledSessInfo); } finally { session.free(); } @@ -334,14 +326,17 @@ OlmDevice.prototype._unpickleSession = function(pickledSession, func) { * store our OlmSession in the session store * * @param {string} deviceKey - * @param {OlmSession} session + * @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int} * @param {*} txn Opaque transaction object from cryptoStore.doTxn() * @private */ -OlmDevice.prototype._saveSession = function(deviceKey, session, txn) { - const pickledSession = session.pickle(this._pickleKey); +OlmDevice.prototype._saveSession = function(deviceKey, sessionInfo, txn) { + const sessionId = sessionInfo.session.session_id(); + const pickledSessionInfo = Object.assign(sessionInfo, { + session: sessionInfo.session.pickle(this._pickleKey), + }); this._cryptoStore.storeEndToEndSession( - deviceKey, session.session_id(), pickledSession, txn, + deviceKey, sessionId, pickledSessionInfo, txn, ); }; @@ -354,7 +349,7 @@ OlmDevice.prototype._saveSession = function(deviceKey, session, txn) { * @private */ OlmDevice.prototype._getUtility = function(func) { - const utility = new Olm.Utility(); + const utility = new global.Olm.Utility(); try { return func(utility); } finally { @@ -466,12 +461,19 @@ OlmDevice.prototype.createOutboundSession = async function( ], (txn) => { this._getAccount(txn, (account) => { - const session = new Olm.Session(); + const session = new global.Olm.Session(); try { session.create_outbound(account, theirIdentityKey, theirOneTimeKey); newSessionId = session.session_id(); this._storeAccount(txn, account); - this._saveSession(theirIdentityKey, session, txn); + const sessionInfo = { + session, + // Pretend we've received a message at this point, otherwise + // if we try to send a message to the device, it won't use + // this session + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirIdentityKey, sessionInfo, txn); } finally { session.free(); } @@ -510,7 +512,7 @@ OlmDevice.prototype.createInboundSession = async function( ], (txn) => { this._getAccount(txn, (account) => { - const session = new Olm.Session(); + const session = new global.Olm.Session(); try { session.create_inbound_from( account, theirDeviceIdentityKey, ciphertext, @@ -520,7 +522,13 @@ OlmDevice.prototype.createInboundSession = async function( const payloadString = session.decrypt(messageType, ciphertext); - this._saveSession(theirDeviceIdentityKey, session, txn); + const sessionInfo = { + session, + // this counts as a received message: set last received message time + // to now + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); result = { payload: payloadString, @@ -568,13 +576,30 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK * @return {Promise} session id, or null if no established session */ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) { - const sessionIds = await this.getSessionIdsForDevice(theirDeviceIdentityKey); - if (sessionIds.length === 0) { + const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey); + if (sessionInfos.length === 0) { return null; } - // Use the session with the lowest ID. - sessionIds.sort(); - return sessionIds[0]; + // Use the session that has most recently received a message + let idxOfBest = 0; + for (let i = 1; i < sessionInfos.length; i++) { + const thisSessInfo = sessionInfos[i]; + const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ? + 0 : thisSessInfo.lastReceivedMessageTs; + + const bestSessInfo = sessionInfos[idxOfBest]; + const bestLastReceived = bestSessInfo.lastReceivedMessageTs === undefined ? + 0 : bestSessInfo.lastReceivedMessageTs; + if ( + thisLastReceived > bestLastReceived || ( + thisLastReceived === bestLastReceived && + thisSessInfo.sessionId < bestSessInfo.sessionId + ) + ) { + idxOfBest = i; + } + } + return sessionInfos[idxOfBest].sessionId; }; /** @@ -597,9 +622,10 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) this._cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, (sessions) => { const sessionIds = Object.keys(sessions).sort(); for (const sessionId of sessionIds) { - this._unpickleSession(sessions[sessionId], (session) => { + this._unpickleSession(sessions[sessionId], (sessInfo) => { info.push({ - hasReceivedMessage: session.has_received_message(), + lastReceivedMessageTs: sessInfo.lastReceivedMessageTs, + hasReceivedMessage: sessInfo.session.has_received_message(), sessionId: sessionId, }); }); @@ -630,9 +656,9 @@ OlmDevice.prototype.encryptMessage = async function( await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { - res = session.encrypt(payloadString); - this._saveSession(theirDeviceIdentityKey, session, txn); + this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { + res = sessionInfo.session.encrypt(payloadString); + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); }); }, ); @@ -657,9 +683,10 @@ OlmDevice.prototype.decryptMessage = async function( await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { - payloadString = session.decrypt(messageType, ciphertext); - this._saveSession(theirDeviceIdentityKey, session, txn); + this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { + payloadString = sessionInfo.session.decrypt(messageType, ciphertext); + sessionInfo.lastReceivedMessageTs = Date.now(); + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); }); }, ); @@ -689,8 +716,8 @@ OlmDevice.prototype.matchesSession = async function( await this._cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { - this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { - matches = session.matches_inbound(ciphertext); + this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => { + matches = sessionInfo.session.matches_inbound(ciphertext); }); }, ); @@ -724,11 +751,11 @@ OlmDevice.prototype._saveOutboundGroupSession = function(session) { */ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) { const pickled = this._outboundGroupSessionStore[sessionId]; - if (pickled === null) { + if (pickled === undefined) { throw new Error("Unknown outbound group session " + sessionId); } - const session = new Olm.OutboundGroupSession(); + const session = new global.Olm.OutboundGroupSession(); try { session.unpickle(this._pickleKey, pickled); return func(session); @@ -744,7 +771,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) { * @return {string} sessionId for the outbound session. */ OlmDevice.prototype.createOutboundGroupSession = function() { - const session = new Olm.OutboundGroupSession(); + const session = new global.Olm.OutboundGroupSession(); try { session.create(); this._saveOutboundGroupSession(session); @@ -816,7 +843,7 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) { * @return {*} result of func */ OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) { - const session = new Olm.InboundGroupSession(); + const session = new global.Olm.InboundGroupSession(); try { session.unpickle(this._pickleKey, sessionData.session); return func(session); @@ -889,7 +916,7 @@ OlmDevice.prototype.addInboundGroupSession = async function( roomId, senderKey, sessionId, txn, (existingSession, existingSessionData) => { if (existingSession) { - console.log( + logger.log( "Update for megolm session " + senderKey + "/" + sessionId, ); // for now we just ignore updates. TODO: implement something here @@ -897,7 +924,7 @@ OlmDevice.prototype.addInboundGroupSession = async function( } // new session. - const session = new Olm.InboundGroupSession(); + const session = new global.Olm.InboundGroupSession(); try { if (exportFormat) { session.import_session(sessionKey); @@ -1034,7 +1061,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se } if (roomId !== sessionData.room_id) { - console.warn( + logger.warn( `requested keys for inbound group session ${senderKey}|` + `${sessionId}, with incorrect room_id ` + `(expected ${sessionData.room_id}, ` + @@ -1058,6 +1085,8 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se * @param {string} roomId room in which the message was received * @param {string} senderKey base64-encoded curve25519 key of the sender * @param {string} sessionId session identifier + * @param {integer} chainIndex The chain index at which to export the session. + * If omitted, export at the first index we know about. * * @returns {Promise<{chain_index: number, key: string, * forwarding_curve25519_key_chain: Array, @@ -1065,9 +1094,12 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se * }>} * details of the session key. The key is a base64-encoded megolm key in * export format. + * + * @throws Error If the given chain index could not be obtained from the known + * index (ie. the given chain index is before the first we have). */ OlmDevice.prototype.getInboundGroupSessionKey = async function( - roomId, senderKey, sessionId, + roomId, senderKey, sessionId, chainIndex, ) { let result; await this._cryptoStore.doTxn( @@ -1078,14 +1110,19 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function( result = null; return; } - const messageIndex = session.first_known_index(); + + if (chainIndex === undefined) { + chainIndex = session.first_known_index(); + } + + const exportedSession = session.export_session(chainIndex); const claimedKeys = sessionData.keysClaimed || {}; const senderEd25519Key = claimedKeys.ed25519 || null; result = { - "chain_index": messageIndex, - "key": session.export_session(messageIndex), + "chain_index": chainIndex, + "key": exportedSession, "forwarding_curve25519_key_chain": sessionData.forwardingCurve25519KeyChain || [], "sender_claimed_ed25519_key": senderEd25519Key, @@ -1119,6 +1156,7 @@ OlmDevice.prototype.exportInboundGroupSession = function( "session_id": sessionId, "session_key": session.export_session(messageIndex), "forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [], + "first_known_index": session.first_known_index(), }; }); }; diff --git a/src/crypto/OutgoingRoomKeyRequestManager.js b/src/crypto/OutgoingRoomKeyRequestManager.js index 75ab35454..bfde6019b 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.js +++ b/src/crypto/OutgoingRoomKeyRequestManager.js @@ -16,6 +16,7 @@ limitations under the License. import Promise from 'bluebird'; +import logger from '../logger'; import utils from '../utils'; /** @@ -108,7 +109,7 @@ export default class OutgoingRoomKeyRequestManager { * Called when the client is stopped. Stops any running background processes. */ stop() { - console.log('stopping OutgoingRoomKeyRequestManager'); + logger.log('stopping OutgoingRoomKeyRequestManager'); // stop the timer on the next run this._clientRunning = false; } @@ -173,7 +174,7 @@ export default class OutgoingRoomKeyRequestManager { // may have seen it, so we still need to send a cancellation // in that case :/ - console.log( + logger.log( 'deleting unnecessary room key request for ' + stringifyRequestBody(requestBody), ); @@ -201,7 +202,7 @@ export default class OutgoingRoomKeyRequestManager { // the request cancelled. There is no point in // sending another cancellation since the other tab // will do it. - console.log( + logger.log( 'Tried to cancel room key request for ' + stringifyRequestBody(requestBody) + ' but it was already cancelled in another tab', @@ -222,7 +223,7 @@ export default class OutgoingRoomKeyRequestManager { updatedReq, andResend, ).catch((e) => { - console.error( + logger.error( "Error sending room key request cancellation;" + " will retry later.", e, ); @@ -243,6 +244,21 @@ export default class OutgoingRoomKeyRequestManager { }); } + /** + * Look for room key requests by target device and state + * + * @param {string} userId Target user ID + * @param {string} deviceId Target device ID + * + * @return {Promise} resolves to a list of all the + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + */ + getOutgoingSentRoomKeyRequest(userId, deviceId) { + return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget( + userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT], + ); + } + // start the background timer to send queued requests, if the timer isn't // already running _startTimer() { @@ -261,7 +277,7 @@ export default class OutgoingRoomKeyRequestManager { }).catch((e) => { // this should only happen if there is an indexeddb error, // in which case we're a bit stuffed anyway. - console.warn( + logger.warn( `error in OutgoingRoomKeyRequestManager: ${e}`, ); }).done(); @@ -282,7 +298,7 @@ export default class OutgoingRoomKeyRequestManager { return Promise.resolve(); } - console.log("Looking for queued outgoing room key requests"); + logger.log("Looking for queued outgoing room key requests"); return this._cryptoStore.getOutgoingRoomKeyRequestByState([ ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, @@ -290,7 +306,7 @@ export default class OutgoingRoomKeyRequestManager { ROOM_KEY_REQUEST_STATES.UNSENT, ]).then((req) => { if (!req) { - console.log("No more outgoing room key requests"); + logger.log("No more outgoing room key requests"); this._sendOutgoingRoomKeyRequestsTimer = null; return; } @@ -312,7 +328,7 @@ export default class OutgoingRoomKeyRequestManager { // go around the loop again return this._sendOutgoingRoomKeyRequests(); }).catch((e) => { - console.error("Error sending room key request; will retry later.", e); + logger.error("Error sending room key request; will retry later.", e); this._sendOutgoingRoomKeyRequestsTimer = null; this._startTimer(); }).done(); @@ -321,7 +337,7 @@ export default class OutgoingRoomKeyRequestManager { // given a RoomKeyRequest, send it and update the request record _sendOutgoingRoomKeyRequest(req) { - console.log( + logger.log( `Requesting keys for ${stringifyRequestBody(req.requestBody)}` + ` from ${stringifyRecipientList(req.recipients)}` + `(id ${req.requestId})`, @@ -347,7 +363,7 @@ export default class OutgoingRoomKeyRequestManager { // Given a RoomKeyRequest, cancel it and delete the request record unless // andResend is set, in which case transition to UNSENT. _sendOutgoingRoomKeyRequestCancellation(req, andResend) { - console.log( + logger.log( `Sending cancellation for key request for ` + `${stringifyRequestBody(req.requestBody)} to ` + `${stringifyRecipientList(req.recipients)} ` + diff --git a/src/crypto/RoomList.js b/src/crypto/RoomList.js index 5bb437fb7..1f0234548 100644 --- a/src/crypto/RoomList.js +++ b/src/crypto/RoomList.js @@ -71,6 +71,9 @@ export default class RoomList { } async setRoomEncryption(roomId, roomInfo) { + // important that this happens before calling into the store + // as it prevents the Crypto::setRoomEncryption from calling + // this twice for consecutive m.room.encryption events this._roomEncryption[roomId] = roomInfo; await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index d8a807092..f244d7add 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,6 +24,7 @@ limitations under the License. import Promise from 'bluebird'; +const logger = require("../../logger"); const utils = require("../../utils"); const olmlib = require("../olmlib"); const base = require("./base"); @@ -64,7 +66,7 @@ OutboundSessionInfo.prototype.needsRotation = function( if (this.useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs ) { - console.log( + logger.log( "Rotating megolm session after " + this.useCount + " messages, " + sessionLifetime + "ms", ); @@ -102,7 +104,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function( } if (!devicesInRoom.hasOwnProperty(userId)) { - console.log("Starting new session because we shared with " + userId); + logger.log("Starting new session because we shared with " + userId); return true; } @@ -112,7 +114,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function( } if (!devicesInRoom[userId].hasOwnProperty(deviceId)) { - console.log( + logger.log( "Starting new session because we shared with " + userId + ":" + deviceId, ); @@ -142,6 +144,11 @@ function MegolmEncryption(params) { // room). this._setupPromise = Promise.resolve(); + // Map of outbound sessions by sessions ID. Used if we need a particular + // session (the session we're currently using to send is always obtained + // using _setupPromise). + this._outboundSessions = {}; + // default rotation periods this._sessionRotationPeriodMsgs = 100; this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000; @@ -181,7 +188,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) { if (session && session.needsRotation(self._sessionRotationPeriodMsgs, self._sessionRotationPeriodMs) ) { - console.log("Starting new megolm session because we need to rotate."); + logger.log("Starting new megolm session because we need to rotate."); session = null; } @@ -191,8 +198,9 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) { } if (!session) { - console.log(`Starting new megolm session for room ${self._roomId}`); + logger.log(`Starting new megolm session for room ${self._roomId}`); session = await self._prepareNewSession(); + self._outboundSessions[session.sessionId] = session; } // now check if we need to share with any devices @@ -262,6 +270,18 @@ MegolmEncryption.prototype._prepareNewSession = async function() { key.key, {ed25519: this._olmDevice.deviceEd25519Key}, ); + if (this._crypto.backupInfo) { + // don't wait for it to complete + this._crypto.backupGroupSession( + this._roomId, this._olmDevice.deviceCurve25519Key, [], + sessionId, key.key, + ).catch((e) => { + // This throws if the upload failed, but this is fine + // since it will have written it to the db and will retry. + console.log("Failed to back up group session", e); + }); + } + return new OutboundSessionInfo(sessionId); }; @@ -318,7 +338,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( continue; } - console.log( + logger.log( "share keys with device " + userId + ":" + deviceId, ); @@ -407,8 +427,98 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function( }; /** - * @private + * Re-shares a megolm session key with devices if the key has already been + * sent to them. * + * @param {string} senderKey The key of the originating device for the session + * @param {string} sessionId ID of the outbound session to share + * @param {string} userId ID of the user who owns the target device + * @param {module:crypto/deviceinfo} device The target device + */ +MegolmEncryption.prototype.reshareKeyWithDevice = async function( + senderKey, sessionId, userId, device, +) { + const obSessionInfo = this._outboundSessions[sessionId]; + if (!obSessionInfo) { + logger.debug("Session ID " + sessionId + " not found: not re-sharing keys"); + return; + } + + // The chain index of the key we previously sent this device + if (obSessionInfo.sharedWithDevices[userId] === undefined) { + logger.debug("Session ID " + sessionId + " never shared with user " + userId); + return; + } + const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId]; + if (sentChainIndex === undefined) { + logger.debug( + "Session ID " + sessionId + " never shared with device " + + userId + ":" + device.deviceId, + ); + return; + } + + // get the key from the inbound session: the outbound one will already + // have been ratcheted to the next chain index. + const key = await this._olmDevice.getInboundGroupSessionKey( + this._roomId, senderKey, sessionId, sentChainIndex, + ); + + if (!key) { + logger.warn( + "No outbound session key found for " + sessionId + ": not re-sharing keys", + ); + return; + } + + await olmlib.ensureOlmSessionsForDevices( + this._olmDevice, this._baseApis, { + [userId]: { + [device.deviceId]: device, + }, + }, + ); + + const payload = { + type: "m.forwarded_room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: this._roomId, + session_id: sessionId, + session_key: key.key, + chain_index: key.chain_index, + sender_key: senderKey, + sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, + forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain, + }, + }; + + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._userId, + this._deviceId, + this._olmDevice, + userId, + device, + payload, + ), + + await this._baseApis.sendToDevice("m.room.encrypted", { + [userId]: { + [device.deviceId]: encryptedContent, + }, + }); + logger.debug( + `Re-shared key for session ${sessionId} with ${userId}:${device.deviceId}`, + ); +}; + +/** * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * * @param {object} devicesByUser @@ -440,10 +550,10 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device await this._encryptAndSendKeysToDevices( session, key.chain_index, userDeviceMaps[i], payload, ); - console.log(`Completed megolm keyshare in ${this._roomId} ` + logger.log(`Completed megolm keyshare in ${this._roomId} ` + `(slice ${i + 1}/${userDeviceMaps.length})`); } catch (e) { - console.log(`megolm keyshare in ${this._roomId} ` + logger.log(`megolm keyshare in ${this._roomId} ` + `(slice ${i + 1}/${userDeviceMaps.length}) failed`); throw e; @@ -462,7 +572,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device */ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) { const self = this; - console.log(`Starting to encrypt event for ${this._roomId}`); + logger.log(`Starting to encrypt event for ${this._roomId}`); return this._getDevicesInRoom(room).then(function(devicesInRoom) { // check if any of these devices are not yet known to the user. @@ -488,6 +598,8 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) { session_id: session.sessionId, // Include our device ID so that recipients can send us a // m.new_device message if they don't have our session key. + // XXX: Do we still need this now that m.new_device messages + // no longer exist since #483? device_id: self._deviceId, }; @@ -496,6 +608,16 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) { }); }; +/** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * This should not normally be necessary. + */ +MegolmEncryption.prototype.forceDiscardSession = function() { + this._setupPromise = this._setupPromise.then(() => null); +}; + /** * Checks the devices we're about to send to and see if any are entirely * unknown to the user. If so, warn the user, and mark them as known to @@ -535,8 +657,9 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) { * @return {module:client.Promise} Promise which resolves to a map * from userId to deviceId to deviceInfo */ -MegolmEncryption.prototype._getDevicesInRoom = function(room) { - const roomMembers = utils.map(room.getEncryptionTargetMembers(), function(u) { +MegolmEncryption.prototype._getDevicesInRoom = async function(room) { + const members = await room.getEncryptionTargetMembers(); + const roomMembers = utils.map(members, function(u) { return u.userId; }); @@ -549,35 +672,31 @@ MegolmEncryption.prototype._getDevicesInRoom = function(room) { // We are happy to use a cached version here: we assume that if we already // have a list of the user's devices, then we already share an e2e room // with them, which means that they will have announced any new devices via - // an m.new_device. - // - // XXX: what if the cache is stale, and the user left the room we had in - // common and then added new devices before joining this one? --Matthew - // - // yup, see https://github.com/vector-im/riot-web/issues/2305 --richvdh - return this._crypto.downloadKeys(roomMembers, false).then((devices) => { - // remove any blocked devices - for (const userId in devices) { - if (!devices.hasOwnProperty(userId)) { + // device_lists in their /sync response. This cache should then be maintained + // using all the device_lists changes and left fields. + // See https://github.com/vector-im/riot-web/issues/2305 for details. + const devices = await this._crypto.downloadKeys(roomMembers, false); + // remove any blocked devices + for (const userId in devices) { + if (!devices.hasOwnProperty(userId)) { + continue; + } + + const userDevices = devices[userId]; + for (const deviceId in userDevices) { + if (!userDevices.hasOwnProperty(deviceId)) { continue; } - const userDevices = devices[userId]; - for (const deviceId in userDevices) { - if (!userDevices.hasOwnProperty(deviceId)) { - continue; - } - - if (userDevices[deviceId].isBlocked() || - (userDevices[deviceId].isUnverified() && isBlacklisting) - ) { - delete userDevices[deviceId]; - } + if (userDevices[deviceId].isBlocked() || + (userDevices[deviceId].isUnverified() && isBlacklisting) + ) { + delete userDevices[deviceId]; } } + } - return devices; - }); + return devices; }; /** @@ -772,12 +891,12 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { !sessionId || !content.session_key ) { - console.error("key event is missing fields"); + logger.error("key event is missing fields"); return; } if (!senderKey) { - console.error("key event has no sender key (not encrypted?)"); + logger.error("key event has no sender key (not encrypted?)"); return; } @@ -794,13 +913,13 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { senderKey = content.sender_key; if (!senderKey) { - console.error("forwarded_room_key event is missing sender_key field"); + logger.error("forwarded_room_key event is missing sender_key field"); return; } const ed25519Key = content.sender_claimed_ed25519_key; if (!ed25519Key) { - console.error( + logger.error( `forwarded_room_key_event is missing sender_claimed_ed25519_key field`, ); return; @@ -813,8 +932,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { keysClaimed = event.getKeysClaimed(); } - console.log(`Adding key for megolm session ${senderKey}|${sessionId}`); - this._olmDevice.addInboundGroupSession( + logger.log(`Adding key for megolm session ${senderKey}|${sessionId}`); + return this._olmDevice.addInboundGroupSession( content.room_id, senderKey, forwardingKeyChain, sessionId, content.session_key, keysClaimed, exportFormat, @@ -829,8 +948,21 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { // have another go at decrypting events sent with this session. this._retryDecryption(senderKey, sessionId); + }).then(() => { + if (this._crypto.backupInfo) { + // don't wait for the keys to be backed up for the server + this._crypto.backupGroupSession( + content.room_id, senderKey, forwardingKeyChain, + content.session_id, content.session_key, keysClaimed, + exportFormat, + ).catch((e) => { + // This throws if the upload failed, but this is fine + // since it will have written it to the db and will retry. + console.log("Failed to back up group session", e); + }); + } }).catch((e) => { - console.error(`Error handling m.room_key_event: ${e}`); + logger.error(`Error handling m.room_key_event: ${e}`); }); }; @@ -872,7 +1004,7 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) { return null; } - console.log( + logger.log( "sharing keys for session " + body.sender_key + "|" + body.session_id + " with device " + userId + ":" + deviceId, @@ -946,6 +1078,22 @@ MegolmDecryption.prototype.importRoomKey = function(session) { session.sender_claimed_keys, true, ).then(() => { + if (this._crypto.backupInfo) { + // don't wait for it to complete + this._crypto.backupGroupSession( + session.room_id, + session.sender_key, + session.forwarding_curve25519_key_chain, + session.session_id, + session.session_key, + session.sender_claimed_keys, + true, + ).catch((e) => { + // This throws if the upload failed, but this is fine + // since it will have written it to the db and will retry. + console.log("Failed to back up group session", e); + }); + } // have another go at decrypting events sent with this session. this._retryDecryption(session.sender_key, session.session_id); }); diff --git a/src/crypto/algorithms/olm.js b/src/crypto/algorithms/olm.js index 26a4d90f8..2ef4bdebb 100644 --- a/src/crypto/algorithms/olm.js +++ b/src/crypto/algorithms/olm.js @@ -22,6 +22,7 @@ limitations under the License. */ import Promise from 'bluebird'; +const logger = require("../../logger"); const utils = require("../../utils"); const olmlib = require("../olmlib"); const DeviceInfo = require("../deviceinfo"); @@ -83,60 +84,62 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) { * * @return {module:client.Promise} Promise which resolves to the new event body */ -OlmEncryption.prototype.encryptMessage = function(room, eventType, content) { +OlmEncryption.prototype.encryptMessage = async function(room, eventType, content) { // pick the list of recipients based on the membership list. // // TODO: there is a race condition here! What if a new user turns up // just as you are sending a secret message? - const users = utils.map(room.getEncryptionTargetMembers(), function(u) { + const members = await room.getEncryptionTargetMembers(); + + const users = utils.map(members, function(u) { return u.userId; }); const self = this; - return this._ensureSession(users).then(function() { - const payloadFields = { - room_id: room.roomId, - type: eventType, - content: content, - }; + await this._ensureSession(users); - const encryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: self._olmDevice.deviceCurve25519Key, - ciphertext: {}, - }; + const payloadFields = { + room_id: room.roomId, + type: eventType, + content: content, + }; - const promises = []; + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: self._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; - for (let i = 0; i < users.length; ++i) { - const userId = users[i]; - const devices = self._crypto.getStoredDevicesForUser(userId); + const promises = []; - for (let j = 0; j < devices.length; ++j) { - const deviceInfo = devices[j]; - const key = deviceInfo.getIdentityKey(); - if (key == self._olmDevice.deviceCurve25519Key) { - // don't bother sending to ourself - continue; - } - if (deviceInfo.verified == DeviceVerification.BLOCKED) { - // don't bother setting up sessions with blocked users - continue; - } + for (let i = 0; i < users.length; ++i) { + const userId = users[i]; + const devices = self._crypto.getStoredDevicesForUser(userId); - promises.push( - olmlib.encryptMessageForDevice( - encryptedContent.ciphertext, - self._userId, self._deviceId, self._olmDevice, - userId, deviceInfo, payloadFields, - ), - ); + for (let j = 0; j < devices.length; ++j) { + const deviceInfo = devices[j]; + const key = deviceInfo.getIdentityKey(); + if (key == self._olmDevice.deviceCurve25519Key) { + // don't bother sending to ourself + continue; + } + if (deviceInfo.verified == DeviceVerification.BLOCKED) { + // don't bother setting up sessions with blocked users + continue; } - } - return Promise.all(promises).return(encryptedContent); - }); + promises.push( + olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + self._userId, self._deviceId, self._olmDevice, + userId, deviceInfo, payloadFields, + ), + ); + } + } + + return await Promise.all(promises).return(encryptedContent); }; /** @@ -271,7 +274,7 @@ OlmDecryption.prototype._decryptMessage = async function( const payload = await this._olmDevice.decryptMessage( theirDeviceIdentityKey, sessionId, message.type, message.body, ); - console.log( + logger.log( "Decrypted Olm message from " + theirDeviceIdentityKey + " with session " + sessionId, ); @@ -326,7 +329,7 @@ OlmDecryption.prototype._decryptMessage = async function( ); } - console.log( + logger.log( "created new inbound Olm session ID " + res.session_id + " with " + theirDeviceIdentityKey, ); diff --git a/src/crypto/backup_password.js b/src/crypto/backup_password.js new file mode 100644 index 000000000..1a6d1f284 --- /dev/null +++ b/src/crypto/backup_password.js @@ -0,0 +1,81 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { randomString } from '../randomstring'; + +const DEFAULT_ITERATIONS = 500000; + +export async function keyForExistingBackup(backupData, password) { + if (!global.Olm) { + throw new Error("Olm is not available"); + } + + const authData = backupData.auth_data; + + if (!authData.private_key_salt || !authData.private_key_iterations) { + throw new Error( + "Salt and/or iterations not found: " + + "this backup cannot be restored with a passphrase", + ); + } + + return await deriveKey( + password, backupData.auth_data.private_key_salt, + backupData.auth_data.private_key_iterations, + ); +} + +export async function keyForNewBackup(password) { + if (!global.Olm) { + throw new Error("Olm is not available"); + } + + const salt = randomString(32); + + const key = await deriveKey(password, salt, DEFAULT_ITERATIONS); + + return { key, salt, iterations: DEFAULT_ITERATIONS }; +} + +async function deriveKey(password, salt, iterations) { + const subtleCrypto = global.crypto.subtle; + const TextEncoder = global.TextEncoder; + if (!subtleCrypto || !TextEncoder) { + // TODO: Implement this for node + throw new Error("Password-based backup is not avaiable on this platform"); + } + + const key = await subtleCrypto.importKey( + 'raw', + new TextEncoder().encode(password), + {name: 'PBKDF2'}, + false, + ['deriveBits'], + ); + + const keybits = await subtleCrypto.deriveBits( + { + name: 'PBKDF2', + salt: new TextEncoder().encode(salt), + iterations: iterations, + hash: 'SHA-512', + }, + key, + global.Olm.PRIVATE_KEY_LENGTH * 8, + ); + + return new Uint8Array(keybits); +} diff --git a/src/crypto/index.js b/src/crypto/index.js index 4a9cbf7eb..17abf8933 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -25,6 +25,7 @@ const anotherjson = require('another-json'); import Promise from 'bluebird'; import {EventEmitter} from 'events'; +const logger = require("../logger"); const utils = require("../utils"); const OlmDevice = require("./OlmDevice"); const olmlib = require("./olmlib"); @@ -36,6 +37,12 @@ const DeviceList = require('./DeviceList').default; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; +export function isCryptoAvailable() { + return Boolean(global.Olm); +} + +const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; + /** * Cryptography bits * @@ -62,7 +69,7 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; * * @param {RoomList} roomList An initialised RoomList object */ -function Crypto(baseApis, sessionStore, userId, deviceId, +export default function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList) { this._baseApis = baseApis; this._sessionStore = sessionStore; @@ -72,6 +79,14 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this._cryptoStore = cryptoStore; this._roomList = roomList; + // track whether this device's megolm keys are being backed up incrementally + // to the server or not. + // XXX: this should probably have a single source of truth from OlmAccount + this.backupInfo = null; // The info dict from /room_keys/version + this.backupKey = null; // The encryption key object + this._checkedForBackup = false; // Have we checked the server for a backup we can use? + this._sendingBackups = false; // Are we currently sending backups? + this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._deviceList = new DeviceList( baseApis, cryptoStore, sessionStore, this._olmDevice, @@ -106,6 +121,24 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this._receivedRoomKeyRequestCancellations = []; // true if we are currently processing received room key requests this._processingRoomKeyRequests = false; + // controls whether device tracking is delayed + // until calling encryptEvent or trackRoomDevices, + // or done immediately upon enabling room encryption. + this._lazyLoadMembers = false; + // in case _lazyLoadMembers is true, + // track if an initial tracking of all the room members + // has happened for a given room. This is delayed + // to avoid loading room members as long as possible. + this._roomDeviceTrackingState = {}; + + // The timestamp of the last time we forced establishment + // of a new session for each device, in milliseconds. + // { + // userId: { + // deviceId: 1234567890000, + // }, + // } + this._lastNewSessionForced = {}; } utils.inherits(Crypto, EventEmitter); @@ -115,6 +148,8 @@ utils.inherits(Crypto, EventEmitter); * Returns a promise which resolves once the crypto module is ready for use. */ Crypto.prototype.init = async function() { + await global.Olm.init(); + const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount()); let cryptoStoreHasAccount; await this._cryptoStore.doTxn( @@ -165,6 +200,126 @@ Crypto.prototype.init = async function() { ); this._deviceList.saveIfDirty(); } + + this._checkAndStartKeyBackup(); +}; + +/** + * Check the server for an active key backup and + * if one is present and has a valid signature from + * one of the user's verified devices, start backing up + * to it. + */ +Crypto.prototype._checkAndStartKeyBackup = async function() { + console.log("Checking key backup status..."); + if (this._baseApis.isGuest()) { + console.log("Skipping key backup check since user is guest"); + this._checkedForBackup = true; + return; + } + let backupInfo; + try { + backupInfo = await this._baseApis.getKeyBackupVersion(); + } catch (e) { + console.log("Error checking for active key backup", e); + if (e.httpStatus / 100 === 4) { + // well that's told us. we won't try again. + this._checkedForBackup = true; + } + return; + } + this._checkedForBackup = true; + + const trustInfo = await this.isKeyBackupTrusted(backupInfo); + + if (trustInfo.usable && !this.backupInfo) { + console.log("Found usable key backup: enabling key backups"); + this._baseApis.enableKeyBackup(backupInfo); + } else if (!trustInfo.usable && this.backupInfo) { + console.log("No usable key backup: disabling key backup"); + this._baseApis.disableKeyBackup(); + } else if (!trustInfo.usable && !this.backupInfo) { + console.log("No usable key backup: not enabling key backup"); + } +}; + +/** + * Forces a re-check of the key backup and enables/disables it + * as appropriate + * + * @param {object} backupInfo Backup info from /room_keys/version endpoint + */ +Crypto.prototype.checkKeyBackup = async function(backupInfo) { + this._checkedForBackup = false; + await this._checkAndStartKeyBackup(); +}; + +/** + * @param {object} backupInfo key backup info dict from /room_keys/version + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool], + * device: [DeviceInfo], + * ] + * } + */ +Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { + const ret = { + usable: false, + sigs: [], + }; + + if ( + !backupInfo || + !backupInfo.algorithm || + !backupInfo.auth_data || + !backupInfo.auth_data.public_key || + !backupInfo.auth_data.signatures + ) { + console.log("Key backup is absent or missing required data"); + return ret; + } + + const mySigs = backupInfo.auth_data.signatures[this._userId]; + if (!mySigs || mySigs.length === 0) { + console.log("Ignoring key backup because it lacks any signatures from this user"); + return ret; + } + + for (const keyId of Object.keys(mySigs)) { + const device = this._deviceList.getStoredDevice( + this._userId, keyId.split(':')[1], // XXX: is this how we're supposed to get the device ID? + ); + if (!device) { + console.log("Ignoring signature from unknown key " + keyId); + continue; + } + const sigInfo = { device }; + try { + await olmlib.verifySignature( + this._olmDevice, + backupInfo.auth_data, + this._userId, + device.deviceId, + device.getFingerprint(), + ); + sigInfo.valid = true; + } catch (e) { + console.log("Bad signature from device " + device.deviceId, e); + sigInfo.valid = false; + } + ret.sigs.push(sigInfo); + } + + ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified()); + return ret; +}; + +/** + */ +Crypto.prototype.enableLazyLoading = function() { + this._lazyLoadMembers = true; }; /** @@ -181,7 +336,7 @@ Crypto.prototype.registerEventHandlers = function(eventEmitter) { try { crypto._onRoomMembership(event, member, oldMembership); } catch (e) { - console.error("Error handling membership change:", e); + logger.error("Error handling membership change:", e); } }); @@ -199,6 +354,7 @@ Crypto.prototype.start = function() { /** Stop background processes related to crypto */ Crypto.prototype.stop = function() { this._outgoingRoomKeyRequestManager.stop(); + this._deviceList.stop(); }; /** @@ -366,7 +522,7 @@ function _maybeUploadOneTimeKeys(crypto) { // create any more keys. return uploadLoop(keyCount); }).catch((e) => { - console.error("Error uploading one-time keys", e.stack || e); + logger.error("Error uploading one-time keys", e.stack || e); }).finally(() => { // reset _oneTimeKeyCount to prevent start uploading based on old data. // it will be set again on the next /sync-response @@ -573,7 +729,7 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { // identity key of the device which set up the Megolm session. const device = this._deviceList.getDeviceByIdentityKey( - event.getSender(), algorithm, senderKey, + algorithm, senderKey, ); if (device === null) { @@ -591,13 +747,13 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { const claimedKey = event.getClaimedEd25519Key(); if (!claimedKey) { - console.warn("Event " + event.getId() + " claims no ed25519 key: " + + logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device"); return null; } if (claimedKey !== device.getFingerprint()) { - console.warn( + logger.warn( "Event " + event.getId() + " claims ed25519 key " + claimedKey + "but sender device has key " + device.getFingerprint()); return null; @@ -606,6 +762,23 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { return device; }; +/** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * @param {string} roomId The ID of the room to discard the session for + * + * This should not normally be necessary. + */ +Crypto.prototype.forceDiscardSession = function(roomId) { + const alg = this._roomEncryptors[roomId]; + if (alg === undefined) throw new Error("Room not encrypted"); + if (alg.forceDiscardSession === undefined) { + throw new Error("Room encryption algorithm doesn't support session discarding"); + } + alg.forceDiscardSession(); +}; + /** * Configure a room to use encryption (ie, save a flag in the sessionstore). * @@ -614,25 +787,49 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { * @param {object} config The encryption config for the room. * * @param {boolean=} inhibitDeviceQuery true to suppress device list query for - * users in the room (for now) + * users in the room (for now). In case lazy loading is enabled, + * the device query is always inhibited as the members are not tracked. */ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { - // if we already have encryption in this room, we should ignore this event - // (for now at least. maybe we should alert the user somehow?) + // if state is being replayed from storage, we might already have a configuration + // for this room as they are persisted as well. + // We just need to make sure the algorithm is initialized in this case. + // However, if the new config is different, + // we should bail out as room encryption can't be changed once set. const existingConfig = this._roomList.getRoomEncryption(roomId); - if (existingConfig && JSON.stringify(existingConfig) != JSON.stringify(config)) { - console.error("Ignoring m.room.encryption event which requests " + - "a change of config in " + roomId); + if (existingConfig) { + if (JSON.stringify(existingConfig) != JSON.stringify(config)) { + logger.error("Ignoring m.room.encryption event which requests " + + "a change of config in " + roomId); + return; + } + } + // if we already have encryption in this room, we should ignore this event, + // as it would reset the encryption algorithm. + // This is at least expected to be called twice, as sync calls onCryptoEvent + // for both the timeline and state sections in the /sync response, + // the encryption event would appear in both. + // If it's called more than twice though, + // it signals a bug on client or server. + const existingAlg = this._roomEncryptors[roomId]; + if (existingAlg) { return; } + // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption + // because it first stores in memory. We should await the promise only + // after all the in-memory state (_roomEncryptors and _roomList) has been updated + // to avoid races when calling this method multiple times. Hence keep a hold of the promise. + let storeConfigPromise = null; + if(!existingConfig) { + storeConfigPromise = this._roomList.setRoomEncryption(roomId, config); + } + const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm]; if (!AlgClass) { throw new Error("Unable to encrypt with " + config.algorithm); } - await this._roomList.setRoomEncryption(roomId, config); - const alg = new AlgClass({ userId: this._userId, deviceId: this._deviceId, @@ -644,24 +841,59 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic }); this._roomEncryptors[roomId] = alg; - // make sure we are tracking the device lists for all users in this room. - console.log("Enabling encryption in " + roomId + "; " + - "starting to track device lists for all users therein"); - const room = this._clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to enable encryption in unknown room ${roomId}`); + if (storeConfigPromise) { + await storeConfigPromise; } - const members = room.getEncryptionTargetMembers(); - members.forEach((m) => { - this._deviceList.startTrackingDeviceList(m.userId); - }); - if (!inhibitDeviceQuery) { - this._deviceList.refreshOutdatedDeviceLists(); + if (!this._lazyLoadMembers) { + logger.log("Enabling encryption in " + roomId + "; " + + "starting to track device lists for all users therein"); + + await this.trackRoomDevices(roomId); + // TODO: this flag is only not used from MatrixClient::setRoomEncryption + // which is never used (inside riot at least) + // but didn't want to remove it as it technically would + // be a breaking change. + if(!this.inhibitDeviceQuery) { + this._deviceList.refreshOutdatedDeviceLists(); + } + } else { + logger.log("Enabling encryption in " + roomId); } }; +/** + * Make sure we are tracking the device lists for all users in this room. + * + * @param {string} roomId The room ID to start tracking devices in. + * @returns {Promise} when all devices for the room have been fetched and marked to track + */ +Crypto.prototype.trackRoomDevices = function(roomId) { + const trackMembers = async () => { + // not an encrypted room + if (!this._roomEncryptors[roomId]) { + return; + } + const room = this._clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); + } + logger.log(`Starting to track devices for room ${roomId} ...`); + const members = await room.getEncryptionTargetMembers(); + members.forEach((m) => { + this._deviceList.startTrackingDeviceList(m.userId); + }); + }; + + let promise = this._roomDeviceTrackingState[roomId]; + if (!promise) { + promise = trackMembers(); + this._roomDeviceTrackingState[roomId] = promise; + } + return promise; +}; + /** * @typedef {Object} module:crypto~OlmSessionResult * @property {module:crypto/deviceinfo} device device info @@ -724,6 +956,7 @@ Crypto.prototype.exportRoomKeys = async function() { const sess = this._olmDevice.exportInboundGroupSession( s.senderKey, s.sessionId, s.sessionData, ); + delete sess.first_known_index; sess.algorithm = olmlib.MEGOLM_ALGORITHM; exportedSessions.push(sess); }); @@ -743,7 +976,7 @@ Crypto.prototype.importRoomKeys = function(keys) { return Promise.map( keys, (key) => { if (!key.room_id || !key.algorithm) { - console.warn("ignoring room key entry with missing fields", key); + logger.warn("ignoring room key entry with missing fields", key); return null; } @@ -753,6 +986,133 @@ Crypto.prototype.importRoomKeys = function(keys) { ); }; +Crypto.prototype._maybeSendKeyBackup = async function(delay, retry) { + if (retry === undefined) retry = true; + + if (!this._sendingBackups) { + this._sendingBackups = true; + try { + if (delay === undefined) { + // by default, wait between 0 and 10 seconds, to avoid backup + // requests from different clients hitting the server all at + // the same time when a new key is sent + delay = Math.random() * 10000; + } + await Promise.delay(delay); + let numFailures = 0; // number of consecutive failures + while (1) { + if (!this.backupKey) { + return; + } + // FIXME: figure out what limit is reasonable + const sessions = await this._cryptoStore.getSessionsNeedingBackup(10); + if (!sessions.length) { + return; + } + const data = {}; + for (const session of sessions) { + const roomId = session.sessionData.room_id; + if (data[roomId] === undefined) { + data[roomId] = {sessions: {}}; + } + + const sessionData = await this._olmDevice.exportInboundGroupSession( + session.senderKey, session.sessionId, session.sessionData, + ); + sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; + delete sessionData.session_id; + delete sessionData.room_id; + const firstKnownIndex = sessionData.first_known_index; + delete sessionData.first_known_index; + const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); + + const forwardedCount = + (sessionData.forwarding_curve25519_key_chain || []).length; + + const device = this._deviceList.getDeviceByIdentityKey( + olmlib.MEGOLM_ALGORITHM, session.senderKey, + ); + + data[roomId]['sessions'][session.sessionId] = { + first_message_index: firstKnownIndex, + forwarded_count: forwardedCount, + is_verified: !!(device && device.isVerified()), + session_data: encrypted, + }; + } + + try { + await this._baseApis.sendKeyBackup( + undefined, undefined, this.backupInfo.version, + {rooms: data}, + ); + numFailures = 0; + await this._cryptoStore.unmarkSessionsNeedingBackup(sessions); + } catch (err) { + numFailures++; + console.log("send failed", err); + if (err.httpStatus === 400 + || err.httpStatus === 403 + || err.httpStatus === 401 + || !retry) { + // retrying probably won't help much, so we should give up + // FIXME: disable backups? + throw err; + } + } + if (numFailures) { + // exponential backoff if we have failures + await new Promise((resolve, reject) => { + setTimeout( + resolve, + 1000 * Math.pow(2, Math.min(numFailures - 1, 4)), + ); + }); + } + } + } finally { + this._sendingBackups = false; + } + } +}; + +Crypto.prototype.backupGroupSession = async function( + roomId, senderKey, forwardingCurve25519KeyChain, + sessionId, sessionKey, keysClaimed, + exportFormat, +) { + if (!this.backupInfo) { + throw new Error("Key backups are not enabled"); + } + + await this._cryptoStore.markSessionsNeedingBackup([{ + senderKey: senderKey, + sessionId: sessionId, + }]); + + await this._maybeSendKeyBackup(); +}; + +Crypto.prototype.backupAllGroupSessions = async function(version) { + await this._cryptoStore.doTxn( + 'readwrite', + [ + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_BACKUP, + ], + (txn) => { + this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { + if (session !== null) { + this._cryptoStore.markSessionsNeedingBackup([session], txn); + } + }); + }, + ); + + await this._maybeSendKeyBackup(0, false); +}; + +/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 /** * Encrypt an event according to the configuration of the room. * @@ -763,7 +1123,8 @@ Crypto.prototype.importRoomKeys = function(keys) { * @return {module:client.Promise?} Promise which resolves when the event has been * encrypted, or null if nothing was needed */ -Crypto.prototype.encryptEvent = function(event, room) { +/* eslint-enable valid-jsdoc */ +Crypto.prototype.encryptEvent = async function(event, room) { if (!room) { throw new Error("Cannot send encrypted messages in unknown rooms"); } @@ -781,6 +1142,12 @@ Crypto.prototype.encryptEvent = function(event, room) { ); } + if (!this._roomDeviceTrackingState[roomId]) { + this.trackRoomDevices(roomId); + } + // wait for all the room devices to be loaded + await this._roomDeviceTrackingState[roomId]; + let content = event.getContent(); // If event has an m.relates_to then we need // to put this on the wrapping event instead @@ -791,20 +1158,19 @@ Crypto.prototype.encryptEvent = function(event, room) { delete content['m.relates_to']; } - return alg.encryptMessage( - room, event.getType(), content, - ).then((encryptedContent) => { - if (mRelatesTo) { - encryptedContent['m.relates_to'] = mRelatesTo; - } + const encryptedContent = await alg.encryptMessage( + room, event.getType(), content); - event.makeEncrypted( - "m.room.encrypted", - encryptedContent, - this._olmDevice.deviceCurve25519Key, - this._olmDevice.deviceEd25519Key, - ); - }); + if (mRelatesTo) { + encryptedContent['m.relates_to'] = mRelatesTo; + } + + event.makeEncrypted( + "m.room.encrypted", + encryptedContent, + this._olmDevice.deviceCurve25519Key, + this._olmDevice.deviceEd25519Key, + ); }; /** @@ -852,7 +1218,7 @@ Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLi // If we didn't make this assumption, we'd have to use the /keys/changes API // to get key changes between the sync token in the device list and the 'old' // sync token used here to make sure we didn't miss any. - this._evalDeviceListChanges(syncDeviceLists); + await this._evalDeviceListChanges(syncDeviceLists); }; /** @@ -866,7 +1232,7 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) { requestBody, recipients, ).catch((e) => { // this normally means we couldn't talk to the store - console.error( + logger.error( 'Error requesting key for event', e, ); }).done(); @@ -883,7 +1249,7 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) { Crypto.prototype.cancelRoomKeyRequest = function(requestBody, andResend) { this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody, andResend) .catch((e) => { - console.warn("Error clearing pending room key requests", e); + logger.warn("Error clearing pending room key requests", e); }).done(); }; @@ -901,7 +1267,7 @@ Crypto.prototype.onCryptoEvent = async function(event) { // finished processing the sync, in onSyncCompleted. await this.setRoomEncryption(roomId, content, true); } catch (e) { - console.error("Error configuring encryption in room " + roomId + + logger.error("Error configuring encryption in room " + roomId + ":", e); } }; @@ -917,8 +1283,9 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) { // scratch, so mark everything as untracked. onCryptoEvent will // be called for all e2e rooms during the processing of the sync, // at which point we'll start tracking all the users of that room. - console.log("Initial sync performed - resetting device tracking state"); + logger.log("Initial sync performed - resetting device tracking state"); this._deviceList.stopTrackingAllDeviceLists(); + this._roomDeviceTrackingState = {}; } }; @@ -964,11 +1331,12 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { }); } - if (deviceLists.left && Array.isArray(deviceLists.left)) { + if (deviceLists.left && Array.isArray(deviceLists.left) && + deviceLists.left.length) { // Check we really don't share any rooms with these users // any more: the server isn't required to give us the // exact correct set. - const e2eUserIds = new Set(this._getE2eUsers()); + const e2eUserIds = new Set(await this._getTrackedE2eUsers()); deviceLists.left.forEach((u) => { if (!e2eUserIds.has(u)) { @@ -980,13 +1348,14 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { /** * Get a list of all the IDs of users we share an e2e room with + * for which we are tracking devices already * * @returns {string[]} List of user IDs */ -Crypto.prototype._getE2eUsers = function() { +Crypto.prototype._getTrackedE2eUsers = async function() { const e2eUserIds = []; - for (const room of this._getE2eRooms()) { - const members = room.getEncryptionTargetMembers(); + for (const room of this._getTrackedE2eRooms()) { + const members = await room.getEncryptionTargetMembers(); for (const member of members) { e2eUserIds.push(member.userId); } @@ -995,27 +1364,25 @@ Crypto.prototype._getE2eUsers = function() { }; /** - * Get a list of the e2e-enabled rooms we are members of + * Get a list of the e2e-enabled rooms we are members of, + * and for which we are already tracking the devices * * @returns {module:models.Room[]} */ -Crypto.prototype._getE2eRooms = function() { +Crypto.prototype._getTrackedE2eRooms = function() { return this._clientStore.getRooms().filter((room) => { // check for rooms with encryption enabled const alg = this._roomEncryptors[room.roomId]; if (!alg) { return false; } - - // ignore any rooms which we have left - const me = room.getMember(this._userId); - if (!me || ( - me.membership !== "join" && me.membership !== "invite" - )) { + if (!this._roomDeviceTrackingState[room.roomId]) { return false; } - return true; + // ignore any rooms which we have left + const myMembership = room.getMyMembership(); + return myMembership === "join" || myMembership === "invite"; }); }; @@ -1027,6 +1394,8 @@ Crypto.prototype._onToDeviceEvent = function(event) { this._onRoomKeyEvent(event); } else if (event.getType() == "m.room_key_request") { this._onRoomKeyRequestEvent(event); + } else if (event.getContent().msgtype === "m.bad.encrypted") { + this._onToDeviceBadEncrypted(event); } else if (event.isBeingDecrypted()) { // once the event has been decrypted, try again event.once('Event.decrypted', (ev) => { @@ -1034,7 +1403,7 @@ Crypto.prototype._onToDeviceEvent = function(event) { }); } } catch (e) { - console.error("Error handling toDeviceEvent:", e); + logger.error("Error handling toDeviceEvent:", e); } }; @@ -1048,14 +1417,109 @@ Crypto.prototype._onRoomKeyEvent = function(event) { const content = event.getContent(); if (!content.room_id || !content.algorithm) { - console.error("key event is missing fields"); + logger.error("key event is missing fields"); return; } + if (!this._checkedForBackup) { + // don't bother awaiting on this - the important thing is that we retry if we + // haven't managed to check before + this._checkAndStartKeyBackup(); + } + const alg = this._getRoomDecryptor(content.room_id, content.algorithm); alg.onRoomKeyEvent(event); }; +/** + * Handle a toDevice event that couldn't be decrypted + * + * @private + * @param {module:models/event.MatrixEvent} event undecryptable event + */ +Crypto.prototype._onToDeviceBadEncrypted = async function(event) { + const content = event.getWireContent(); + const sender = event.getSender(); + const algorithm = content.algorithm; + const deviceKey = content.sender_key; + + if (sender === undefined || deviceKey === undefined || deviceKey === undefined) { + return; + } + + // check when we last forced a new session with this device: if we've already done so + // recently, don't do it again. + this._lastNewSessionForced[sender] = this._lastNewSessionForced[sender] || {}; + const lastNewSessionForced = this._lastNewSessionForced[sender][deviceKey] || 0; + if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) { + logger.debug( + "New session already forced with device " + sender + ":" + deviceKey + + " at " + lastNewSessionForced + ": not forcing another", + ); + return; + } + + // establish a new olm session with this device since we're failing to decrypt messages + // on a current session. + // Note that an undecryptable message from another device could easily be spoofed - + // is there anything we can do to mitigate this? + const device = this._deviceList.getDeviceByIdentityKey(algorithm, deviceKey); + if (!device) { + logger.info( + "Couldn't find device for identity key " + deviceKey + + ": not re-establishing session", + ); + return; + } + const devicesByUser = {}; + devicesByUser[sender] = [device]; + await olmlib.ensureOlmSessionsForDevices( + this._olmDevice, this._baseApis, devicesByUser, true, + ); + + this._lastNewSessionForced[sender][deviceKey] = Date.now(); + + // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // is the session it has most recently received a message on). + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._userId, + this._deviceId, + this._olmDevice, + sender, + device, + {type: "m.dummy"}, + ); + + await this._baseApis.sendToDevice("m.room.encrypted", { + [sender]: { + [device.deviceId]: encryptedContent, + }, + }); + + + // Most of the time this probably won't be necessary since we'll have queued up a key request when + // we failed to decrypt the message and will be waiting a bit for the key to arrive before sending + // it. This won't always be the case though so we need to re-send any that have already been sent + // to avoid races. + const requestsToResend = + await this._outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest( + sender, device.deviceId, + ); + for (const keyReq of requestsToResend) { + this.cancelRoomKeyRequest(keyReq.requestBody, true); + } +}; + /** * Handle a change in the membership state of a member of a room * @@ -1080,15 +1544,20 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) { // not encrypting in this room return; } - - if (member.membership == 'join') { - console.log('Join event for ' + member.userId + ' in ' + roomId); - // make sure we are tracking the deviceList for this user - this._deviceList.startTrackingDeviceList(member.userId); - } else if (member.membership == 'invite' && - this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { - console.log('Invite event for ' + member.userId + ' in ' + roomId); - this._deviceList.startTrackingDeviceList(member.userId); + // only mark users in this room as tracked if we already started tracking in this room + // this way we don't start device queries after sync on behalf of this room which we won't use + // the result of anyway, as we'll need to do a query again once all the members are fetched + // by calling _trackRoomDevices + if (this._roomDeviceTrackingState[roomId]) { + if (member.membership == 'join') { + logger.log('Join event for ' + member.userId + ' in ' + roomId); + // make sure we are tracking the deviceList for this user + this._deviceList.startTrackingDeviceList(member.userId); + } else if (member.membership == 'invite' && + this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { + logger.log('Invite event for ' + member.userId + ' in ' + roomId); + this._deviceList.startTrackingDeviceList(member.userId); + } } alg.onRoomMembership(event, member, oldMembership); @@ -1153,7 +1622,7 @@ Crypto.prototype._processReceivedRoomKeyRequests = async function() { this._processReceivedRoomKeyRequestCancellation(cancellation), ); } catch (e) { - console.error(`Error processing room key requsts: ${e}`); + logger.error(`Error processing room key requsts: ${e}`); } finally { this._processingRoomKeyRequests = false; } @@ -1172,13 +1641,31 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { const roomId = body.room_id; const alg = body.algorithm; - console.log(`m.room_key_request from ${userId}:${deviceId}` + + logger.log(`m.room_key_request from ${userId}:${deviceId}` + ` for ${roomId} / ${body.session_id} (id ${req.requestId})`); if (userId !== this._userId) { - // TODO: determine if we sent this device the keys already: in - // which case we can do so again. - console.log("Ignoring room key request from other user for now"); + if (!this._roomEncryptors[roomId]) { + logger.debug(`room key request for unencrypted room ${roomId}`); + return; + } + const encryptor = this._roomEncryptors[roomId]; + const device = this._deviceList.getStoredDevice(userId, deviceId); + if (!device) { + logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`); + return; + } + + try { + await encryptor.reshareKeyWithDevice( + body.sender_key, body.session_id, userId, device, + ); + } catch (e) { + logger.warn( + "Failed to re-share keys for session " + body.session_id + + " with device " + userId + ":" + device.deviceId, e, + ); + } return; } @@ -1188,18 +1675,18 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { // if we don't have a decryptor for this room/alg, we don't have // the keys for the requested events, and can drop the requests. if (!this._roomDecryptors[roomId]) { - console.log(`room key request for unencrypted room ${roomId}`); + logger.log(`room key request for unencrypted room ${roomId}`); return; } const decryptor = this._roomDecryptors[roomId][alg]; if (!decryptor) { - console.log(`room key request for unknown alg ${alg} in room ${roomId}`); + logger.log(`room key request for unknown alg ${alg} in room ${roomId}`); return; } if (!await decryptor.hasKeysForKeyRequest(req)) { - console.log( + logger.log( `room key request for unknown session ${roomId} / ` + body.session_id, ); @@ -1213,7 +1700,7 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { // if the device is is verified already, share the keys const device = this._deviceList.getStoredDevice(userId, deviceId); if (device && device.isVerified()) { - console.log('device is already verified: sharing keys'); + logger.log('device is already verified: sharing keys'); req.share(); return; } @@ -1230,7 +1717,7 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { Crypto.prototype._processReceivedRoomKeyRequestCancellation = async function( cancellation, ) { - console.log( + logger.log( `m.room_key_request cancellation for ${cancellation.userId}:` + `${cancellation.deviceId} (id ${cancellation.requestId})`, ); @@ -1415,6 +1902,3 @@ class IncomingRoomKeyRequestCancellation { * @event module:client~MatrixClient#"crypto.warning" * @param {string} type One of the strings listed above */ - -/** */ -module.exports = Crypto; diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 56799c513..4ee89bf8d 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -23,6 +23,7 @@ limitations under the License. import Promise from 'bluebird'; const anotherjson = require('another-json'); +const logger = require("../logger"); const utils = require("../utils"); /** @@ -35,6 +36,11 @@ module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2"; */ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2"; +/** + * matrix algorithm tag for megolm backups + */ +module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2"; + /** * Encrypt an event payload for an Olm device @@ -65,7 +71,7 @@ module.exports.encryptMessageForDevice = async function( return; } - console.log( + logger.log( "Using sessionid " + sessionId + " for device " + recipientUserId + ":" + recipientDevice.deviceId, ); @@ -115,14 +121,17 @@ module.exports.encryptMessageForDevice = async function( * @param {module:base-apis~MatrixBaseApis} baseApis * * @param {object} devicesByUser - * map from userid to list of devices + * map from userid to list of devices to ensure sessions for + * + * @param {bolean} force If true, establish a new session even if one already exists. + * Optional. * * @return {module:client.Promise} resolves once the sessions are complete, to * an Object mapping from userId to deviceId to * {@link module:crypto~OlmSessionResult} */ module.exports.ensureOlmSessionsForDevices = async function( - olmDevice, baseApis, devicesByUser, + olmDevice, baseApis, devicesByUser, force, ) { const devicesWithoutSession = [ // [userId, deviceId], ... @@ -140,7 +149,7 @@ module.exports.ensureOlmSessionsForDevices = async function( const deviceId = deviceInfo.deviceId; const key = deviceInfo.getIdentityKey(); const sessionId = await olmDevice.getSessionIdForDevice(key); - if (sessionId === null) { + if (sessionId === null || force) { devicesWithoutSession.push([userId, deviceId]); } result[userId][deviceId] = { @@ -176,7 +185,7 @@ module.exports.ensureOlmSessionsForDevices = async function( for (let j = 0; j < devices.length; j++) { const deviceInfo = devices[j]; const deviceId = deviceInfo.deviceId; - if (result[userId][deviceId].sessionId) { + if (result[userId][deviceId].sessionId && !force) { // we already have a result for this device continue; } @@ -190,7 +199,7 @@ module.exports.ensureOlmSessionsForDevices = async function( } if (!oneTimeKey) { - console.warn( + logger.warn( "No one-time keys (alg=" + oneTimeKeyAlgorithm + ") for device " + userId + ":" + deviceId, ); @@ -219,7 +228,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn deviceInfo.getFingerprint(), ); } catch (e) { - console.error( + logger.error( "Unable to verify signature on one-time key for device " + userId + ":" + deviceId + ":", e, ); @@ -233,12 +242,12 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn ); } catch (e) { // possibly a bad key - console.error("Error starting session with device " + + logger.error("Error starting session with device " + userId + ":" + deviceId + ": " + e); return null; } - console.log("Started new sessionid " + sid + + logger.log("Started new sessionid " + sid + " for device " + userId + ":" + deviceId); return sid; } diff --git a/src/crypto/recoverykey.js b/src/crypto/recoverykey.js new file mode 100644 index 000000000..d4d949f42 --- /dev/null +++ b/src/crypto/recoverykey.js @@ -0,0 +1,66 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import bs58 from 'bs58'; + +// picked arbitrarily but to try & avoid clashing with any bitcoin ones +// (which are also base58 encoded, but bitcoin's involve a lot more hashing) +const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; + +export function encodeRecoveryKey(key) { + const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1); + buf.set(OLM_RECOVERY_KEY_PREFIX, 0); + buf.set(key, OLM_RECOVERY_KEY_PREFIX.length); + + let parity = 0; + for (let i = 0; i < buf.length - 1; ++i) { + parity ^= buf[i]; + } + buf[buf.length - 1] = parity; + const base58key = bs58.encode(buf); + + return base58key.match(/.{1,4}/g).join(" "); +} + +export function decodeRecoveryKey(recoverykey) { + const result = bs58.decode(recoverykey.replace(/ /g, '')); + + let parity = 0; + for (const b of result) { + parity ^= b; + } + if (parity !== 0) { + throw new Error("Incorrect parity"); + } + + for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) { + if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) { + throw new Error("Incorrect prefix"); + } + } + + if ( + result.length !== + OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1 + ) { + throw new Error("Incorrect length"); + } + + return result.slice( + OLM_RECOVERY_KEY_PREFIX.length, + OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH, + ); +} diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 4a7f48789..566ef9384 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -16,9 +16,11 @@ limitations under the License. */ import Promise from 'bluebird'; + +import logger from '../../logger'; import utils from '../../utils'; -export const VERSION = 6; +export const VERSION = 7; /** * Implementation of a CryptoStore which is backed by an existing @@ -38,7 +40,7 @@ export class Backend { // attempts to delete the database will block (and subsequent // attempts to re-create it will also block). db.onversionchange = (ev) => { - console.log(`versionchange for indexeddb ${this._dbName}: closing`); + logger.log(`versionchange for indexeddb ${this._dbName}: closing`); db.close(); }; } @@ -64,7 +66,7 @@ export class Backend { this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => { if (existing) { // this entry matches the request - return it. - console.log( + logger.log( `already have key request outstanding for ` + `${requestBody.room_id} / ${requestBody.session_id}: ` + `not sending another`, @@ -75,7 +77,7 @@ export class Backend { // we got to the end of the list without finding a match // - add the new request. - console.log( + logger.log( `enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id, ); @@ -204,6 +206,42 @@ export class Backend { return promiseifyTxn(txn).then(() => result); } + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + let stateIndex = 0; + const results = []; + + function onsuccess(ev) { + const cursor = ev.target.result; + if (cursor) { + const keyReq = cursor.value; + if (keyReq.recipients.includes({userId, deviceId})) { + results.push(keyReq); + } + cursor.continue(); + } else { + // try the next state in the list + stateIndex++; + if (stateIndex >= wantedStates.length) { + // no matches + return; + } + + const wantedState = wantedStates[stateIndex]; + const cursorReq = ev.target.source.openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + } + } + + const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + const store = txn.objectStore("outgoingRoomKeyRequests"); + + const wantedState = wantedStates[stateIndex]; + const cursorReq = store.index("state").openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + + return promiseifyTxn(txn).then(() => results); + } + /** * Look for an existing room key request by id and state, and update it if * found @@ -226,7 +264,7 @@ export class Backend { } const data = cursor.value; if (data.state != expectedState) { - console.warn( + logger.warn( `Cannot update room key request from ${expectedState} ` + `as it was already updated to ${data.state}`, ); @@ -264,7 +302,7 @@ export class Backend { } const data = cursor.value; if (data.state != expectedState) { - console.warn( + logger.warn( `Cannot delete room key request in state ${data.state} ` + `(expected ${expectedState})`, ); @@ -312,7 +350,10 @@ export class Backend { getReq.onsuccess = function() { const cursor = getReq.result; if (cursor) { - results[cursor.value.sessionId] = cursor.value.session; + results[cursor.value.sessionId] = { + session: cursor.value.session, + lastReceivedMessageTs: cursor.value.lastReceivedMessageTs, + }; cursor.continue(); } else { try { @@ -330,7 +371,10 @@ export class Backend { getReq.onsuccess = function() { try { if (getReq.result) { - func(getReq.result.session); + func({ + session: getReq.result.session, + lastReceivedMessageTs: getReq.result.lastReceivedMessageTs, + }); } else { func(null); } @@ -340,9 +384,14 @@ export class Backend { }; } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const objectStore = txn.objectStore("sessions"); - objectStore.put({deviceKey, sessionId, session}); + objectStore.put({ + deviceKey, + sessionId, + session: sessionInfo.session, + lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs, + }); } // Inbound group sessions @@ -400,7 +449,7 @@ export class Backend { ev.stopPropagation(); // ...and this stops it from aborting the transaction ev.preventDefault(); - console.log( + logger.log( "Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId, ); @@ -460,6 +509,71 @@ export class Backend { }; } + // session backups + + getSessionsNeedingBackup(limit) { + return new Promise((resolve, reject) => { + const sessions = []; + + const txn = this._db.transaction( + ["sessions_needing_backup", "inbound_group_sessions"], + "readonly", + ); + txn.onerror = reject; + txn.oncomplete = function() { + resolve(sessions); + }; + const objectStore = txn.objectStore("sessions_needing_backup"); + const sessionStore = txn.objectStore("inbound_group_sessions"); + const getReq = objectStore.openCursor(); + getReq.onsuccess = function() { + const cursor = getReq.result; + if (cursor) { + const sessionGetReq = sessionStore.get(cursor.key); + sessionGetReq.onsuccess = function() { + sessions.push({ + senderKey: sessionGetReq.result.senderCurve25519Key, + sessionId: sessionGetReq.result.sessionId, + sessionData: sessionGetReq.result.session, + }); + }; + if (!limit || sessions.length < limit) { + cursor.continue(); + } + } + }; + }); + } + + unmarkSessionsNeedingBackup(sessions) { + const txn = this._db.transaction("sessions_needing_backup", "readwrite"); + const objectStore = txn.objectStore("sessions_needing_backup"); + return Promise.all(sessions.map((session) => { + return new Promise((resolve, reject) => { + const req = objectStore.delete([session.senderKey, session.sessionId]); + req.onsuccess = resolve; + req.onerror = reject; + }); + })); + } + + markSessionsNeedingBackup(sessions, txn) { + if (!txn) { + txn = this._db.transaction("sessions_needing_backup", "readwrite"); + } + const objectStore = txn.objectStore("sessions_needing_backup"); + return Promise.all(sessions.map((session) => { + return new Promise((resolve, reject) => { + const req = objectStore.put({ + senderCurve25519Key: session.senderKey, + sessionId: session.sessionId, + }); + req.onsuccess = resolve; + req.onerror = reject; + }); + })); + } + doTxn(mode, stores, func) { const txn = this._db.transaction(stores, mode); const promise = promiseifyTxn(txn); @@ -471,7 +585,7 @@ export class Backend { } export function upgradeDatabase(db, oldVersion) { - console.log( + logger.log( `Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`, ); @@ -498,6 +612,11 @@ export function upgradeDatabase(db, oldVersion) { if (oldVersion < 6) { db.createObjectStore("rooms"); } + if (oldVersion < 7) { + db.createObjectStore("sessions_needing_backup", { + keyPath: ["senderCurve25519Key", "sessionId"], + }); + } // Expand as needed. } diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 249b29b63..bf380b6da 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -17,9 +17,11 @@ limitations under the License. import Promise from 'bluebird'; +import logger from '../../logger'; import LocalStorageCryptoStore from './localStorage-crypto-store'; import MemoryCryptoStore from './memory-crypto-store'; import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend'; +import {InvalidCryptoStoreError} from '../../errors'; /** * Internal module. indexeddb storage for e2e. @@ -64,7 +66,7 @@ export default class IndexedDBCryptoStore { return; } - console.log(`connecting to indexeddb ${this._dbName}`); + logger.log(`connecting to indexeddb ${this._dbName}`); const req = this._indexedDB.open( this._dbName, IndexedDBCryptoStoreBackend.VERSION, @@ -77,7 +79,7 @@ export default class IndexedDBCryptoStore { }; req.onblocked = () => { - console.log( + logger.log( `can't yet open IndexedDBCryptoStore because it is open elsewhere`, ); }; @@ -89,20 +91,42 @@ export default class IndexedDBCryptoStore { req.onsuccess = (r) => { const db = r.target.result; - console.log(`connected to indexeddb ${this._dbName}`); + logger.log(`connected to indexeddb ${this._dbName}`); resolve(new IndexedDBCryptoStoreBackend.Backend(db)); }; + }).then((backend) => { + // Edge has IndexedDB but doesn't support compund keys which we use fairly extensively. + // Try a dummy query which will fail if the browser doesn't support compund keys, so + // we can fall back to a different backend. + return backend.doTxn( + 'readonly', + [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], + (txn) => { + backend.getEndToEndInboundGroupSession('', '', txn, () => {}); + }).then(() => { + return backend; + }, + ); }).catch((e) => { - console.warn( + if (e.name === 'VersionError') { + logger.warn("Crypto DB is too new for us to use!", e); + // don't fall back to a different store: the user has crypto data + // in this db so we should use it or nothing at all. + throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW); + } + logger.warn( `unable to connect to indexeddb ${this._dbName}` + `: falling back to localStorage store: ${e}`, ); - return new LocalStorageCryptoStore(global.localStorage); - }).catch((e) => { - console.warn( - `unable to open localStorage: falling back to in-memory store: ${e}`, - ); - return new MemoryCryptoStore(); + + try { + return new LocalStorageCryptoStore(global.localStorage); + } catch (e) { + logger.warn( + `unable to open localStorage: falling back to in-memory store: ${e}`, + ); + return new MemoryCryptoStore(); + } }); return this._backendPromise; @@ -120,11 +144,11 @@ export default class IndexedDBCryptoStore { return; } - console.log(`Removing indexeddb instance: ${this._dbName}`); + logger.log(`Removing indexeddb instance: ${this._dbName}`); const req = this._indexedDB.deleteDatabase(this._dbName); req.onblocked = () => { - console.log( + logger.log( `can't yet delete IndexedDBCryptoStore because it is open elsewhere`, ); }; @@ -134,14 +158,14 @@ export default class IndexedDBCryptoStore { }; req.onsuccess = () => { - console.log(`Removed indexeddb instance: ${this._dbName}`); + logger.log(`Removed indexeddb instance: ${this._dbName}`); resolve(); }; }).catch((e) => { // in firefox, with indexedDB disabled, this fails with a // DOMError. We treat this as non-fatal, so that people can // still use the app. - console.warn(`unable to delete IndexedDBCryptoStore: ${e}`); + logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`); }); } @@ -193,6 +217,24 @@ export default class IndexedDBCryptoStore { }); } + /** + * Look for room key requests by target device and state + * + * @param {string} userId Target user ID + * @param {string} deviceId Target device ID + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to a list of all the + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + */ + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + return this._connect().then((backend) => { + return backend.getOutgoingRoomKeyRequestsByTarget( + userId, deviceId, wantedStates, + ); + }); + } + /** * Look for an existing room key request by id and state, and update it if * found @@ -270,7 +312,10 @@ export default class IndexedDBCryptoStore { * @param {string} sessionId The ID of the session to retrieve * @param {*} txn An active transaction. See doTxn(). * @param {function(object)} func Called with A map from sessionId - * to Base64 end-to-end session. + * to session information object with 'session' key being the + * Base64 end-to-end session and lastReceivedMessageTs being the + * timestamp in milliseconds at which the session last received + * a message. */ getEndToEndSession(deviceKey, sessionId, txn, func) { this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func); @@ -282,7 +327,10 @@ export default class IndexedDBCryptoStore { * @param {string} deviceKey The public key of the other device. * @param {*} txn An active transaction. See doTxn(). * @param {function(object)} func Called with A map from sessionId - * to Base64 end-to-end session. + * to session information object with 'session' key being the + * Base64 end-to-end session and lastReceivedMessageTs being the + * timestamp in milliseconds at which the session last received + * a message. */ getEndToEndSessions(deviceKey, txn, func) { this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func); @@ -292,12 +340,12 @@ export default class IndexedDBCryptoStore { * Store a session between the logged-in user and another device * @param {string} deviceKey The public key of the other device. * @param {string} sessionId The ID for this end-to-end session. - * @param {string} session Base64 encoded end-to-end session. + * @param {string} sessionInfo Session information object * @param {*} txn An active transaction. See doTxn(). */ - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { this._backendPromise.value().storeEndToEndSession( - deviceKey, sessionId, session, txn, + deviceKey, sessionId, sessionInfo, txn, ); } @@ -407,6 +455,43 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().getEndToEndRooms(txn, func); } + // session backups + + /** + * Get the inbound group sessions that need to be backed up. + * @param {integer} limit The maximum number of sessions to retrieve. 0 + * for no limit. + * @returns {Promise} resolves to an array of inbound group sessions + */ + getSessionsNeedingBackup(limit) { + return this._connect().then((backend) => { + return backend.getSessionsNeedingBackup(limit); + }); + } + + /** + * Unmark sessions as needing to be backed up. + * @param {Array} sessions The sessions that need to be backed up. + * @returns {Promise} resolves when the sessions are unmarked + */ + unmarkSessionsNeedingBackup(sessions) { + return this._connect().then((backend) => { + return backend.unmarkSessionsNeedingBackup(sessions); + }); + } + + /** + * Mark sessions as needing to be backed up. + * @param {Array} sessions The sessions that need to be backed up. + * @param {*} txn An active transaction. See doTxn(). (optional) + * @returns {Promise} resolves when the sessions are marked + */ + markSessionsNeedingBackup(sessions, txn) { + return this._connect().then((backend) => { + return backend.markSessionsNeedingBackup(sessions, txn); + }); + } + /** * Perform a transaction on the crypto store. Any store methods * that require a transaction (txn) object to be passed in may @@ -440,3 +525,4 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; +IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup'; diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 3f2f0d09a..5ca48a16c 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -15,6 +15,8 @@ limitations under the License. */ import Promise from 'bluebird'; + +import logger from '../../logger'; import MemoryCryptoStore from './memory-crypto-store.js'; /** @@ -32,6 +34,7 @@ const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; +const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup"; function keyEndToEndSessions(deviceKey) { return E2E_PREFIX + "sessions/" + deviceKey; @@ -65,7 +68,21 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { } _getEndToEndSessions(deviceKey, txn, func) { - return getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + const fixedSessions = {}; + + // fix up any old sessions to be objects rather than just the base64 pickle + for (const [sid, val] of Object.entries(sessions || {})) { + if (typeof val === 'string') { + fixedSessions[sid] = { + session: val, + }; + } else { + fixedSessions[sid] = val; + } + } + + return fixedSessions; } getEndToEndSession(deviceKey, sessionId, txn, func) { @@ -77,9 +94,9 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { func(this._getEndToEndSessions(deviceKey) || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const sessions = this._getEndToEndSessions(deviceKey) || {}; - sessions[sessionId] = session; + sessions[sessionId] = sessionInfo; setJsonItem( this.store, keyEndToEndSessions(deviceKey), sessions, ); @@ -165,6 +182,58 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { func(result); } + getSessionsNeedingBackup(limit) { + const sessionsNeedingBackup + = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + const sessions = []; + + for (const session in sessionsNeedingBackup) { + if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) { + // see getAllEndToEndInboundGroupSessions for the magic number explanations + const senderKey = session.substr(0, 43); + const sessionId = session.substr(44); + this.getEndToEndInboundGroupSession( + senderKey, sessionId, null, + (sessionData) => { + sessions.push({ + senderKey: senderKey, + sessionId: sessionId, + sessionData: sessionData, + }); + }, + ); + if (limit && session.length >= limit) { + break; + } + } + } + return Promise.resolve(sessions); + } + + unmarkSessionsNeedingBackup(sessions) { + const sessionsNeedingBackup + = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + for (const session of sessions) { + delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; + } + setJsonItem( + this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup, + ); + return Promise.resolve(); + } + + markSessionsNeedingBackup(sessions) { + const sessionsNeedingBackup + = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + for (const session of sessions) { + sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; + } + setJsonItem( + this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup, + ); + return Promise.resolve(); + } + /** * Delete all data from this store. * @@ -199,8 +268,8 @@ function getJsonItem(store, key) { // JSON.parse(null) === null, so this returns null. return JSON.parse(store.getItem(key)); } catch (e) { - console.log("Error: Failed to get key %s: %s", key, e.stack || e); - console.log(e.stack); + logger.log("Error: Failed to get key %s: %s", key, e.stack || e); + logger.log(e.stack); } return null; } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 469cdb49b..03753e0e1 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -17,6 +17,7 @@ limitations under the License. import Promise from 'bluebird'; +import logger from '../../logger'; import utils from '../../utils'; /** @@ -41,6 +42,8 @@ export default class MemoryCryptoStore { this._deviceData = null; // roomId -> Opaque roomInfo object this._rooms = {}; + // Set of {senderCurve25519Key+'/'+sessionId} + this._sessionsNeedingBackup = {}; } /** @@ -71,7 +74,7 @@ export default class MemoryCryptoStore { if (existing) { // this entry matches the request - return it. - console.log( + logger.log( `already have key request outstanding for ` + `${requestBody.room_id} / ${requestBody.session_id}: ` + `not sending another`, @@ -81,7 +84,7 @@ export default class MemoryCryptoStore { // we got to the end of the list without finding a match // - add the new request. - console.log( + logger.log( `enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id, ); @@ -144,6 +147,19 @@ export default class MemoryCryptoStore { return Promise.resolve(null); } + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + const results = []; + + for (const req of this._outgoingRoomKeyRequests) { + for (const state of wantedStates) { + if (req.state === state && req.recipients.includes({userId, deviceId})) { + results.push(req); + } + } + } + return Promise.resolve(results); + } + /** * Look for an existing room key request by id and state, and update it if * found @@ -163,7 +179,7 @@ export default class MemoryCryptoStore { } if (req.state != expectedState) { - console.warn( + logger.warn( `Cannot update room key request from ${expectedState} ` + `as it was already updated to ${req.state}`, ); @@ -194,7 +210,7 @@ export default class MemoryCryptoStore { } if (req.state != expectedState) { - console.warn( + logger.warn( `Cannot delete room key request in state ${req.state} ` + `(expected ${expectedState})`, ); @@ -233,13 +249,13 @@ export default class MemoryCryptoStore { func(this._sessions[deviceKey] || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { let deviceSessions = this._sessions[deviceKey]; if (deviceSessions === undefined) { deviceSessions = {}; this._sessions[deviceKey] = deviceSessions; } - deviceSessions[sessionId] = session; + deviceSessions[sessionId] = sessionInfo; } // Inbound Group Sessions @@ -295,6 +311,41 @@ export default class MemoryCryptoStore { func(this._rooms); } + getSessionsNeedingBackup(limit) { + const sessions = []; + for (const session in this._sessionsNeedingBackup) { + if (this._inboundGroupSessions[session]) { + sessions.push({ + senderKey: session.substr(0, 43), + sessionId: session.substr(44), + sessionData: this._inboundGroupSessions[session], + }); + if (limit && session.length >= limit) { + break; + } + } + } + return Promise.resolve(sessions); + } + + unmarkSessionsNeedingBackup(sessions) { + for (const session of sessions) { + const sessionKey = session.senderKey + '/' + session.sessionId; + delete this._sessionsNeedingBackup[sessionKey]; + } + return Promise.resolve(); + } + + markSessionsNeedingBackup(sessions) { + for (const session of sessions) { + const sessionKey = session.senderKey + '/' + session.sessionId; + this._sessionsNeedingBackup[sessionKey] = true; + } + return Promise.resolve(); + } + + // Session key backups + doTxn(mode, stores, func) { return Promise.resolve(func(null)); } diff --git a/src/errors.js b/src/errors.js new file mode 100644 index 000000000..f3d88d643 --- /dev/null +++ b/src/errors.js @@ -0,0 +1,46 @@ +// can't just do InvalidStoreError extends Error +// because of http://babeljs.io/docs/usage/caveats/#classes +export function InvalidStoreError(reason, value) { + const message = `Store is invalid because ${reason}, ` + + `please stop the client, delete all data and start the client again`; + const instance = Reflect.construct(Error, [message]); + Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); + instance.reason = reason; + instance.value = value; + return instance; +} + +InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING"; + +InvalidStoreError.prototype = Object.create(Error.prototype, { + constructor: { + value: Error, + enumerable: false, + writable: true, + configurable: true, + }, +}); +Reflect.setPrototypeOf(InvalidStoreError, Error); + + +export function InvalidCryptoStoreError(reason) { + const message = `Crypto store is invalid because ${reason}, ` + + `please stop the client, delete all data and start the client again`; + const instance = Reflect.construct(Error, [message]); + Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); + instance.reason = reason; + instance.name = 'InvalidCryptoStoreError'; + return instance; +} + +InvalidCryptoStoreError.TOO_NEW = "TOO_NEW"; + +InvalidCryptoStoreError.prototype = Object.create(Error.prototype, { + constructor: { + value: Error, + enumerable: false, + writable: true, + configurable: true, + }, +}); +Reflect.setPrototypeOf(InvalidCryptoStoreError, Error); diff --git a/src/filter.js b/src/filter.js index e0f03daa7..a63fcee13 100644 --- a/src/filter.js +++ b/src/filter.js @@ -51,6 +51,17 @@ function Filter(userId, filterId) { this.definition = {}; } +Filter.LAZY_LOADING_MESSAGES_FILTER = { + lazy_load_members: true, +}; + +Filter.LAZY_LOADING_SYNC_FILTER = { + room: { + state: Filter.LAZY_LOADING_MESSAGES_FILTER, + }, +}; + + /** * Get the ID of this filter on your homeserver (if known) * @return {?Number} The filter ID diff --git a/src/http-api.js b/src/http-api.js index b753d6bf8..c29250d2a 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -752,6 +752,8 @@ module.exports.MatrixHttpApi.prototype = { method: method, withCredentials: false, qs: queryParams, + qsStringifyOptions: opts.qsStringifyOptions, + useQuerystring: true, body: data, json: false, timeout: localTimeoutMs, diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 000000000..ee77c2918 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,36 @@ +/* +Copyright 2018 AndrĂ© Jaenisch + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module logger + */ +const log = require("loglevel"); + +// This is to demonstrate, that you can use any namespace you want. +// Namespaces allow you to turn on/off the logging for specific parts of the +// application. +// An idea would be to control this via an environment variable (on Node.js). +// See https://www.npmjs.com/package/debug to see how this could be implemented +// Part of #332 is introducing a logging library in the first place. +const DEFAULT_NAME_SPACE = "matrix"; +const logger = log.getLogger(DEFAULT_NAME_SPACE); +logger.setLevel(log.levels.DEBUG); + +/** + * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. + * Can be tailored down to specific use cases if needed. +*/ +module.exports = logger; diff --git a/src/matrix.js b/src/matrix.js index bcc88e0e9..43d1a1c24 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -34,6 +34,8 @@ module.exports.SyncAccumulator = require("./sync-accumulator"); module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi; /** The {@link module:http-api.MatrixError|MatrixError} class. */ module.exports.MatrixError = require("./http-api").MatrixError; +/** The {@link module:errors.InvalidStoreError|InvalidStoreError} class. */ +module.exports.InvalidStoreError = require("./errors").InvalidStoreError; /** The {@link module:client.MatrixClient|MatrixClient} class. */ module.exports.MatrixClient = require("./client").MatrixClient; /** The {@link module:models/room|Room} class. */ @@ -65,6 +67,8 @@ module.exports.Filter = require("./filter"); module.exports.TimelineWindow = require("./timeline-window").TimelineWindow; /** The {@link module:interactive-auth} class. */ module.exports.InteractiveAuth = require("./interactive-auth"); +/** The {@link module:auto-discovery|AutoDiscovery} class. */ +module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery; module.exports.MemoryCryptoStore = diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index 2777ea313..65e15dfc8 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -168,49 +168,19 @@ EventTimelineSet.prototype.resetLiveTimeline = function( // if timeline support is disabled, forget about the old timelines const resetAllTimelines = !this._timelineSupport || !forwardPaginationToken; - let newTimeline; + const oldTimeline = this._liveTimeline; + const newTimeline = resetAllTimelines ? + oldTimeline.forkLive(EventTimeline.FORWARDS) : + oldTimeline.fork(EventTimeline.FORWARDS); + if (resetAllTimelines) { - newTimeline = new EventTimeline(this); this._timelines = [newTimeline]; this._eventIdToTimeline = {}; } else { - newTimeline = this.addTimeline(); + this._timelines.push(newTimeline); } - const oldTimeline = this._liveTimeline; - - // Collect the state events from the old timeline - const evMap = oldTimeline.getState(EventTimeline.FORWARDS).events; - const events = []; - for (const evtype in evMap) { - if (!evMap.hasOwnProperty(evtype)) { - continue; - } - for (const stateKey in evMap[evtype]) { - if (!evMap[evtype].hasOwnProperty(stateKey)) { - continue; - } - events.push(evMap[evtype][stateKey]); - } - } - - // Use those events to initialise the state of the new live timeline - newTimeline.initialiseState(events); - - const freshEndState = newTimeline._endState; - // Now clobber the end state of the new live timeline with that from the - // previous live timeline. It will be identical except that we'll keep - // using the same RoomMember objects for the 'live' set of members with any - // listeners still attached - newTimeline._endState = oldTimeline._endState; - - // If we're not resetting all timelines, we need to fix up the old live timeline - if (!resetAllTimelines) { - // Firstly, we just stole the old timeline's end state, so it needs a new one. - // Just swap them around and give it the one we just generated for the - // new live timeline. - oldTimeline._endState = freshEndState; - + if (forwardPaginationToken) { // Now set the forward pagination token on the old live timeline // so it can be forward-paginated. oldTimeline.setPaginationToken( diff --git a/src/models/event-timeline.js b/src/models/event-timeline.js index 9ddf84e31..d23e483da 100644 --- a/src/models/event-timeline.js +++ b/src/models/event-timeline.js @@ -106,6 +106,50 @@ EventTimeline.prototype.initialiseState = function(stateEvents) { this._endState.setStateEvents(stateEvents); }; +/** + * Forks the (live) timeline, taking ownership of the existing directional state of this timeline. + * All attached listeners will keep receiving state updates from the new live timeline state. + * The end state of this timeline gets replaced with an independent copy of the current RoomState, + * and will need a new pagination token if it ever needs to paginate forwards. + + * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * start of the timeline; EventTimeline.FORWARDS to get the state at the end + * of the timeline. + * + * @return {EventTimeline} the new timeline + */ +EventTimeline.prototype.forkLive = function(direction) { + const forkState = this.getState(direction); + const timeline = new EventTimeline(this._eventTimelineSet); + timeline._startState = forkState.clone(); + // Now clobber the end state of the new live timeline with that from the + // previous live timeline. It will be identical except that we'll keep + // using the same RoomMember objects for the 'live' set of members with any + // listeners still attached + timeline._endState = forkState; + // Firstly, we just stole the current timeline's end state, so it needs a new one. + // Make an immutable copy of the state so back pagination will get the correct sentinels. + this._endState = forkState.clone(); + return timeline; +}; + +/** + * Creates an independent timeline, inheriting the directional state from this timeline. + * + * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * start of the timeline; EventTimeline.FORWARDS to get the state at the end + * of the timeline. + * + * @return {EventTimeline} the new timeline + */ +EventTimeline.prototype.fork = function(direction) { + const forkState = this.getState(direction); + const timeline = new EventTimeline(this._eventTimelineSet); + timeline._startState = forkState.clone(); + timeline._endState = forkState.clone(); + return timeline; +}; + /** * Get the ID of the room for this timeline * @return {string} room ID diff --git a/src/models/room-member.js b/src/models/room-member.js index 02fd5b344..e7a4bf88c 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -58,10 +58,27 @@ function RoomMember(roomId, userId) { this.events = { member: null, }; + this._isOutOfBand = false; this._updateModifiedTime(); } utils.inherits(RoomMember, EventEmitter); +/** + * Mark the member as coming from a channel that is not sync + */ +RoomMember.prototype.markOutOfBand = function() { + this._isOutOfBand = true; +}; + +/** + * @return {bool} does the member come from a channel that is not sync? + * This is used to store the member seperately + * from the sync state so it available across browser sessions. + */ +RoomMember.prototype.isOutOfBand = function() { + return this._isOutOfBand; +}; + /** * Update this room member's membership event. May fire "RoomMember.name" if * this event updates this member's name. @@ -75,13 +92,20 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { if (event.getType() !== "m.room.member") { return; } + + this._isOutOfBand = false; + this.events.member = event; const oldMembership = this.membership; this.membership = event.getDirectionalContent().membership; const oldName = this.name; - this.name = calculateDisplayName(this, event, roomState); + this.name = calculateDisplayName( + this.userId, + event.getDirectionalContent().displayname, + roomState); + this.rawDisplayName = event.getDirectionalContent().displayname || this.userId; if (oldMembership !== this.membership) { this._updateModifiedTime(); @@ -177,6 +201,44 @@ RoomMember.prototype.getLastModifiedTime = function() { return this._modified; }; + +RoomMember.prototype.isKicked = function() { + return this.membership === "leave" && + this.events.member.getSender() !== this.events.member.getStateKey(); +}; + +/** + * If this member was invited with the is_direct flag set, return + * the user that invited this member + * @return {string} user id of the inviter + */ +RoomMember.prototype.getDMInviter = function() { + // when not available because that room state hasn't been loaded in, + // we don't really know, but more likely to not be a direct chat + if (this.events.member) { + // TODO: persist the is_direct flag on the member as more member events + // come in caused by displayName changes. + + // the is_direct flag is set on the invite member event. + // This is copied on the prev_content section of the join member event + // when the invite is accepted. + + const memberEvent = this.events.member; + let memberContent = memberEvent.getContent(); + let inviteSender = memberEvent.getSender(); + + if (memberContent.membership === "join") { + memberContent = memberEvent.getPrevContent(); + inviteSender = memberEvent.getUnsigned().prev_sender; + } + + if (memberContent.membership === "invite" && memberContent.is_direct) { + return inviteSender; + } + } +}; + + /** * Get the avatar URL for a room member. * @param {string} baseUrl The base homeserver URL See @@ -200,10 +262,12 @@ RoomMember.prototype.getAvatarUrl = if (allowDefault === undefined) { allowDefault = true; } - if (!this.events.member && !allowDefault) { + + const rawUrl = this.getMxcAvatarUrl(); + + if (!rawUrl && !allowDefault) { return null; } - const rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null; const httpUrl = ContentRepo.getHttpUriForMxc( baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks, ); @@ -216,12 +280,21 @@ RoomMember.prototype.getAvatarUrl = } return null; }; +/** + * get the mxc avatar url, either from a state event, or from a lazily loaded member + * @return {string} the mxc avatar url + */ +RoomMember.prototype.getMxcAvatarUrl = function() { + if(this.events.member) { + return this.events.member.getDirectionalContent().avatar_url; + } else if(this.user) { + return this.user.avatarUrl; + } + return null; +}; -function calculateDisplayName(member, event, roomState) { - const displayName = event.getDirectionalContent().displayname; - const selfUserId = member.userId; - - if (!displayName) { +function calculateDisplayName(selfUserId, displayName, roomState) { + if (!displayName || displayName === selfUserId) { return selfUserId; } diff --git a/src/models/room-state.js b/src/models/room-state.js index 1ca3388c3..3dbd7ff88 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -22,6 +22,11 @@ const EventEmitter = require("events").EventEmitter; const utils = require("../utils"); const RoomMember = require("./room-member"); +// possible statuses for out-of-band member loading +const OOB_STATUS_NOTSTARTED = 1; +const OOB_STATUS_INPROGRESS = 2; +const OOB_STATUS_FINISHED = 3; + /** * Construct room state. * @@ -46,13 +51,17 @@ const RoomMember = require("./room-member"); * @constructor * @param {?string} roomId Optional. The ID of the room which has this state. * If none is specified it just tracks paginationTokens, useful for notifTimelineSet + * @param {?object} oobMemberFlags Optional. The state of loading out of bound members. + * As the timeline might get reset while they are loading, this state needs to be inherited + * and shared when the room state is cloned for the new timeline. + * This should only be passed from clone. * @prop {Object.} members The room member dictionary, keyed * on the user's ID. * @prop {Object.>} events The state * events dictionary, keyed on the event type and then the state_key value. * @prop {string} paginationToken The pagination token for this state. */ -function RoomState(roomId) { +function RoomState(roomId, oobMemberFlags = undefined) { this.roomId = roomId; this.members = { // userId: RoomMember @@ -70,6 +79,22 @@ function RoomState(roomId) { this._userIdsToDisplayNames = {}; this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite this._joinedMemberCount = null; // cache of the number of joined members + // joined members count from summary api + // once set, we know the server supports the summary api + // and we should only trust that + // we could also only trust that before OOB members + // are loaded but doesn't seem worth the hassle atm + this._summaryJoinedMemberCount = null; + // same for invited member count + this._invitedMemberCount = null; + this._summaryInvitedMemberCount = null; + + if (!oobMemberFlags) { + oobMemberFlags = { + status: OOB_STATUS_NOTSTARTED, + }; + } + this._oobMemberFlags = oobMemberFlags; } utils.inherits(RoomState, EventEmitter); @@ -79,14 +104,48 @@ utils.inherits(RoomState, EventEmitter); * @return {integer} The number of members in this room whose membership is 'join' */ RoomState.prototype.getJoinedMemberCount = function() { + if (this._summaryJoinedMemberCount !== null) { + return this._summaryJoinedMemberCount; + } if (this._joinedMemberCount === null) { - this._joinedMemberCount = this.getMembers().filter((m) => { - return m.membership === 'join'; - }).length; + this._joinedMemberCount = this.getMembers().reduce((count, m) => { + return m.membership === 'join' ? count + 1 : count; + }, 0); } return this._joinedMemberCount; }; +/** + * Set the joined member count explicitly (like from summary part of the sync response) + * @param {number} count the amount of joined members + */ +RoomState.prototype.setJoinedMemberCount = function(count) { + this._summaryJoinedMemberCount = count; +}; +/** + * Returns the number of invited members in this room + * @return {integer} The number of members in this room whose membership is 'invite' + */ +RoomState.prototype.getInvitedMemberCount = function() { + if (this._summaryInvitedMemberCount !== null) { + return this._summaryInvitedMemberCount; + } + if (this._invitedMemberCount === null) { + this._invitedMemberCount = this.getMembers().reduce((count, m) => { + return m.membership === 'invite' ? count + 1 : count; + }, 0); + } + return this._invitedMemberCount; +}; + +/** + * Set the amount of invited members in this room + * @param {number} count the amount of invited members + */ +RoomState.prototype.setInvitedMemberCount = function(count) { + this._summaryInvitedMemberCount = count; +}; + /** * Get all RoomMembers in this room. * @return {Array} A list of RoomMembers. @@ -95,6 +154,16 @@ RoomState.prototype.getMembers = function() { return utils.values(this.members); }; +/** + * Get all RoomMembers in this room, excluding the user IDs provided. + * @param {Array} excludedIds The user IDs to exclude. + * @return {Array} A list of RoomMembers. + */ +RoomState.prototype.getMembersExcept = function(excludedIds) { + return utils.values(this.members) + .filter((m) => !excludedIds.includes(m.userId)); +}; + /** * Get a room member by their user ID. * @param {string} userId The room member's user ID. @@ -119,12 +188,9 @@ RoomState.prototype.getSentinelMember = function(userId) { if (sentinel === undefined) { sentinel = new RoomMember(this.roomId, userId); - const membershipEvent = this.getStateEvents("m.room.member", userId); - if (!membershipEvent) return null; - sentinel.setMembershipEvent(membershipEvent, this); - const pwrLvlEvent = this.getStateEvents("m.room.power_levels", ""); - if (pwrLvlEvent) { - sentinel.setPowerLevelEvent(pwrLvlEvent); + const member = this.members[userId]; + if (member) { + sentinel.setMembershipEvent(member.events.member, this); } this._sentinels[userId] = sentinel; } @@ -152,6 +218,67 @@ RoomState.prototype.getStateEvents = function(eventType, stateKey) { return event ? event : null; }; +/** + * Creates a copy of this room state so that mutations to either won't affect the other. + * @return {RoomState} the copy of the room state + */ +RoomState.prototype.clone = function() { + const copy = new RoomState(this.roomId, this._oobMemberFlags); + + // Ugly hack: because setStateEvents will mark + // members as susperseding future out of bound members + // if loading is in progress (through _oobMemberFlags) + // since these are not new members, we're merely copying them + // set the status to not started + // after copying, we set back the status + const status = this._oobMemberFlags.status; + this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; + + Object.values(this.events).forEach((eventsByStateKey) => { + const eventsForType = Object.values(eventsByStateKey); + copy.setStateEvents(eventsForType); + }); + + // Ugly hack: see above + this._oobMemberFlags.status = status; + + if (this._summaryInvitedMemberCount !== null) { + copy.setInvitedMemberCount(this.getInvitedMemberCount()); + } + if (this._summaryJoinedMemberCount !== null) { + copy.setJoinedMemberCount(this.getJoinedMemberCount()); + } + + // copy out of band flags if needed + if (this._oobMemberFlags.status == OOB_STATUS_FINISHED) { + // copy markOutOfBand flags + this.getMembers().forEach((member) => { + if (member.isOutOfBand()) { + const copyMember = copy.getMember(member.userId); + copyMember.markOutOfBand(); + } + }); + } + + return copy; +}; + +/** + * Add previously unknown state events. + * When lazy loading members while back-paginating, + * the relevant room state for the timeline chunk at the end + * of the chunk can be set with this method. + * @param {MatrixEvent[]} events state events to prepend + */ +RoomState.prototype.setUnknownStateEvents = function(events) { + const unknownStateEvents = events.filter((event) => { + return this.events[event.getType()] === undefined || + this.events[event.getType()][event.getStateKey()] === undefined; + }); + + this.setStateEvents(unknownStateEvents); +}; + /** * Add an array of one or more state MatrixEvents, overwriting * any existing state with the same {type, stateKey} tuple. Will fire @@ -175,10 +302,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) { return; } - if (self.events[event.getType()] === undefined) { - self.events[event.getType()] = {}; - } - self.events[event.getType()][event.getStateKey()] = event; + self._setStateEvent(event); if (event.getType() === "m.room.member") { _updateDisplayNameCache( self, event.getStateKey(), event.getContent().displayname, @@ -216,24 +340,10 @@ RoomState.prototype.setStateEvents = function(stateEvents) { event.getPrevContent().displayname; } - let member = self.members[userId]; - if (!member) { - member = new RoomMember(event.getRoomId(), userId); - self.emit("RoomState.newMember", event, self, member); - } - + const member = self._getOrCreateMember(userId, event); member.setMembershipEvent(event, self); - // this member may have a power level already, so set it. - const pwrLvlEvent = self.getStateEvents("m.room.power_levels", ""); - if (pwrLvlEvent) { - member.setPowerLevelEvent(pwrLvlEvent); - } - // blow away the sentinel which is now outdated - delete self._sentinels[userId]; - - self.members[userId] = member; - self._joinedMemberCount = null; + self._updateMember(member); self.emit("RoomState.members", event, self, member); } else if (event.getType() === "m.room.power_levels") { const members = utils.values(self.members); @@ -248,6 +358,140 @@ RoomState.prototype.setStateEvents = function(stateEvents) { }); }; +/** + * Looks up a member by the given userId, and if it doesn't exist, + * create it and emit the `RoomState.newMember` event. + * This method makes sure the member is added to the members dictionary + * before emitting, as this is done from setStateEvents and _setOutOfBandMember. + * @param {string} userId the id of the user to look up + * @param {MatrixEvent} event the membership event for the (new) member. Used to emit. + * @fires module:client~MatrixClient#event:"RoomState.newMember" + * @returns {RoomMember} the member, existing or newly created. + */ +RoomState.prototype._getOrCreateMember = function(userId, event) { + let member = this.members[userId]; + if (!member) { + member = new RoomMember(this.roomId, userId); + // add member to members before emitting any events, + // as event handlers often lookup the member + this.members[userId] = member; + this.emit("RoomState.newMember", event, this, member); + } + return member; +}; + +RoomState.prototype._setStateEvent = function(event) { + if (this.events[event.getType()] === undefined) { + this.events[event.getType()] = {}; + } + this.events[event.getType()][event.getStateKey()] = event; +}; + +RoomState.prototype._updateMember = function(member) { + // this member may have a power level already, so set it. + const pwrLvlEvent = this.getStateEvents("m.room.power_levels", ""); + if (pwrLvlEvent) { + member.setPowerLevelEvent(pwrLvlEvent); + } + + // blow away the sentinel which is now outdated + delete this._sentinels[member.userId]; + + this.members[member.userId] = member; + this._joinedMemberCount = null; + this._invitedMemberCount = null; +}; + +/** + * Get the out-of-band members loading state, whether loading is needed or not. + * Note that loading might be in progress and hence isn't needed. + * @return {bool} whether or not the members of this room need to be loaded + */ +RoomState.prototype.needsOutOfBandMembers = function() { + return this._oobMemberFlags.status === OOB_STATUS_NOTSTARTED; +}; + +/** + * Mark this room state as waiting for out-of-band members, + * ensuring it doesn't ask for them to be requested again + * through needsOutOfBandMembers + */ +RoomState.prototype.markOutOfBandMembersStarted = function() { + if (this._oobMemberFlags.status !== OOB_STATUS_NOTSTARTED) { + return; + } + this._oobMemberFlags.status = OOB_STATUS_INPROGRESS; +}; + +/** + * Mark this room state as having failed to fetch out-of-band members + */ +RoomState.prototype.markOutOfBandMembersFailed = function() { + if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) { + return; + } + this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; +}; + +/** + * Clears the loaded out-of-band members + */ +RoomState.prototype.clearOutOfBandMembers = function() { + let count = 0; + Object.keys(this.members).forEach((userId) => { + const member = this.members[userId]; + if (member.isOutOfBand()) { + ++count; + delete this.members[userId]; + } + }); + console.log(`LL: RoomState removed ${count} members...`); + this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; +}; + +/** + * Sets the loaded out-of-band members. + * @param {MatrixEvent[]} stateEvents array of membership state events + */ +RoomState.prototype.setOutOfBandMembers = function(stateEvents) { + console.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`); + if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) { + return; + } + console.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`); + this._oobMemberFlags.status = OOB_STATUS_FINISHED; + stateEvents.forEach((e) => this._setOutOfBandMember(e)); +}; + +/** + * Sets a single out of band member, used by both setOutOfBandMembers and clone + * @param {MatrixEvent} stateEvent membership state event + */ +RoomState.prototype._setOutOfBandMember = function(stateEvent) { + if (stateEvent.getType() !== 'm.room.member') { + return; + } + const userId = stateEvent.getStateKey(); + const existingMember = this.getMember(userId); + // never replace members received as part of the sync + if (existingMember && !existingMember.isOutOfBand()) { + return; + } + + const member = this._getOrCreateMember(userId, stateEvent); + member.setMembershipEvent(stateEvent, this); + // needed to know which members need to be stored seperately + // as they are not part of the sync accumulator + // this is cleared by setMembershipEvent so when it's updated through /sync + member.markOutOfBand(); + + _updateDisplayNameCache(this, member.userId, member.name); + + this._setStateEvent(stateEvent); + this._updateMember(member); + this.emit("RoomState.members", stateEvent, this, member); +}; + /** * Set the current typing event for this room. * @param {MatrixEvent} event The typing event @@ -401,11 +645,6 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) { * according to the room's state. */ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) { - const member = this.getMember(userId); - if (!member || member.membership == 'leave') { - return false; - } - const power_levels_event = this.getStateEvents('m.room.power_levels', ''); let power_levels; @@ -413,25 +652,34 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) { let state_default = 0; let events_default = 0; + let powerLevel = 0; if (power_levels_event) { power_levels = power_levels_event.getContent(); events_levels = power_levels.events || {}; - if (utils.isNumber(power_levels.state_default)) { + if (Number.isFinite(power_levels.state_default)) { state_default = power_levels.state_default; } else { state_default = 50; } - if (utils.isNumber(power_levels.events_default)) { + + const userPowerLevel = power_levels.users && power_levels.users[userId]; + if (Number.isFinite(userPowerLevel)) { + powerLevel = userPowerLevel; + } else if(Number.isFinite(power_levels.users_default)) { + powerLevel = power_levels.users_default; + } + + if (Number.isFinite(power_levels.events_default)) { events_default = power_levels.events_default; } } let required_level = state ? state_default : events_default; - if (utils.isNumber(events_levels[eventType])) { + if (Number.isFinite(events_levels[eventType])) { required_level = events_levels[eventType]; } - return member.powerLevel >= required_level; + return powerLevel >= required_level; }; /** @@ -543,7 +791,8 @@ function _updateDisplayNameCache(roomState, userId, displayName) { /** * Fires whenever a member is added to the members dictionary. The RoomMember - * will not be fully populated yet (e.g. no membership state). + * will not be fully populated yet (e.g. no membership state) but will already + * be available in the members dictionary. * @event module:client~MatrixClient#"RoomState.newMember" * @param {MatrixEvent} event The matrix event which caused this event to fire. * @param {RoomState} state The room state whose RoomState.members dictionary diff --git a/src/models/room.js b/src/models/room.js index b426c7490..0a835ec64 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,6 +22,7 @@ const EventEmitter = require("events").EventEmitter; const EventStatus = require("./event").EventStatus; const RoomSummary = require("./room-summary"); +const RoomMember = require("./room-member"); const MatrixEvent = require("./event").MatrixEvent; const utils = require("../utils"); const ContentRepo = require("../content-repo"); @@ -29,6 +31,8 @@ const EventTimelineSet = require("./event-timeline-set"); import ReEmitter from '../ReEmitter'; +const LATEST_ROOM_VERSION = '1'; + function synthesizeReceipt(userId, event, receiptType) { // console.log("synthesizing receipt for "+event.getId()); // This is really ugly because JS has no way to express an object literal @@ -68,6 +72,8 @@ function synthesizeReceipt(userId, event, receiptType) { * @constructor * @alias module:models/room * @param {string} roomId Required. The ID of this room. + * @param {MatrixClient} client Required. The client, used to lazy load members. + * @param {string} myUserId Required. The ID of the syncing user. * @param {Object=} opts Configuration options * @param {*} opts.storageToken Optional. The token which a data store can use * to remember the state of the room. What this means is dependent on the store @@ -102,7 +108,7 @@ function synthesizeReceipt(userId, event, receiptType) { * @prop {*} storageToken A token which a data store can use to remember * the state of the room. */ -function Room(roomId, opts) { +function Room(roomId, client, myUserId, opts) { opts = opts || {}; opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; @@ -115,6 +121,7 @@ function Room(roomId, opts) { ); } + this.myUserId = myUserId; this.roomId = roomId; this.name = roomId; this.tags = { @@ -171,9 +178,56 @@ function Room(roomId, opts) { // read by megolm; boolean value - null indicates "use global value" this._blacklistUnverifiedDevices = null; + this._selfMembership = null; + this._summaryHeroes = null; + // awaited by getEncryptionTargetMembers while room members are loading + + this._client = client; + if (!this._opts.lazyLoadMembers) { + this._membersPromise = Promise.resolve(); + } else { + this._membersPromise = null; + } } + utils.inherits(Room, EventEmitter); +/** + * Gets the version of the room + * @returns {string} The version of the room, or null if it could not be determined + */ +Room.prototype.getVersion = function() { + const createEvent = this.currentState.getStateEvents("m.room.create", ""); + if (!createEvent) { + console.warn("Room " + this.room_id + " does not have an m.room.create event"); + return '1'; + } + const ver = createEvent.getContent()['room_version']; + if (ver === undefined) return '1'; + return ver; +}; + +/** + * Determines whether this room needs to be upgraded to a new version + * @returns {string?} What version the room should be upgraded to, or null if + * the room does not require upgrading at this time. + */ +Room.prototype.shouldUpgradeToVersion = function() { + // This almost certainly won't be the way this actually works - this + // is essentially a stub method. + if (this.getVersion() === LATEST_ROOM_VERSION) return null; + return LATEST_ROOM_VERSION; +}; + +/** + * Determines whether the given user is permitted to perform a room upgrade + * @param {String} userId The ID of the user to test against + * @returns {bool} True if the given user is permitted to upgrade the room + */ +Room.prototype.userMayUpgradeRoom = function(userId) { + return this.currentState.maySendStateEvent("m.room.tombstone", userId); +}; + /** * Get the list of pending sent events for this room * @@ -201,6 +255,232 @@ Room.prototype.getLiveTimeline = function() { return this.getUnfilteredTimelineSet().getLiveTimeline(); }; +/** + * @param {string} myUserId the user id for the logged in member + * @return {string} the membership type (join | leave | invite) for the logged in user + */ +Room.prototype.getMyMembership = function() { + return this._selfMembership; +}; + +/** + * If this room is a DM we're invited to, + * try to find out who invited us + * @return {string} user id of the inviter + */ +Room.prototype.getDMInviter = function() { + if (this.myUserId) { + const me = this.getMember(this.myUserId); + if (me) { + return me.getDMInviter(); + } + } + if (this._selfMembership === "invite") { + // fall back to summary information + const memberCount = this.getInvitedAndJoinedMemberCount(); + if (memberCount == 2 && this._summaryHeroes.length) { + return this._summaryHeroes[0]; + } + } +}; + +/** + * Assuming this room is a DM room, tries to guess with which user. + * @return {string} user id of the other member (could be syncing user) + */ +Room.prototype.guessDMUserId = function() { + const me = this.getMember(this.myUserId); + if (me) { + const inviterId = me.getDMInviter(); + if (inviterId) { + return inviterId; + } + } + // remember, we're assuming this room is a DM, + // so returning the first member we find should be fine + const hasHeroes = Array.isArray(this._summaryHeroes) && + this._summaryHeroes.length; + if (hasHeroes) { + return this._summaryHeroes[0]; + } + const members = this.currentState.getMembers(); + const anyMember = members.find((m) => m.userId !== this.myUserId); + if (anyMember) { + return anyMember.userId; + } + // it really seems like I'm the only user in the room + // so I probably created a room with just me in it + // and marked it as a DM. Ok then + return this.myUserId; +}; + +Room.prototype.getAvatarFallbackMember = function() { + const memberCount = this.getInvitedAndJoinedMemberCount(); + if (memberCount > 2) { + return; + } + const hasHeroes = Array.isArray(this._summaryHeroes) && + this._summaryHeroes.length; + if (hasHeroes) { + const availableMember = this._summaryHeroes.map((userId) => { + return this.getMember(userId); + }).find((member) => !!member); + if (availableMember) { + return availableMember; + } + } + const members = this.currentState.getMembers(); + // could be different than memberCount + // as this includes left members + if (members.length <= 2) { + const availableMember = members.find((m) => { + return m.userId !== this.myUserId; + }); + if (availableMember) { + return availableMember; + } + } + // if all else fails, try falling back to a user, + // and create a one-off member for it + if (hasHeroes) { + const availableUser = this._summaryHeroes.map((userId) => { + return this._client.getUser(userId); + }).find((user) => !!user); + if (availableUser) { + const member = new RoomMember( + this.roomId, availableUser.userId); + member.user = availableUser; + return member; + } + } +}; + +/** + * Sets the membership this room was received as during sync + * @param {string} membership join | leave | invite + */ +Room.prototype.updateMyMembership = function(membership) { + const prevMembership = this._selfMembership; + this._selfMembership = membership; + if (prevMembership !== membership) { + if (membership === "leave") { + this._cleanupAfterLeaving(); + } + this.emit("Room.myMembership", this, membership, prevMembership); + } +}; + +Room.prototype._loadMembersFromServer = async function() { + const lastSyncToken = this._client.store.getSyncToken(); + const queryString = utils.encodeParams({ + not_membership: "leave", + at: lastSyncToken, + }); + const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, + {$roomId: this.roomId}); + const http = this._client._http; + const response = await http.authedRequest(undefined, "GET", path); + return response.chunk; +}; + + +Room.prototype._loadMembers = async function() { + // were the members loaded from the server? + let fromServer = false; + let rawMembersEvents = + await this._client.store.getOutOfBandMembers(this.roomId); + if (rawMembersEvents === null) { + fromServer = true; + rawMembersEvents = await this._loadMembersFromServer(); + console.log(`LL: got ${rawMembersEvents.length} ` + + `members from server for room ${this.roomId}`); + } + const memberEvents = rawMembersEvents.map(this._client.getEventMapper()); + return {memberEvents, fromServer}; +}; + +/** + * Preloads the member list in case lazy loading + * of memberships is in use. Can be called multiple times, + * it will only preload once. + * @return {Promise} when preloading is done and + * accessing the members on the room will take + * all members in the room into account + */ +Room.prototype.loadMembersIfNeeded = function() { + if (this._membersPromise) { + return this._membersPromise; + } + + // mark the state so that incoming messages while + // the request is in flight get marked as superseding + // the OOB members + this.currentState.markOutOfBandMembersStarted(); + + const inMemoryUpdate = this._loadMembers().then((result) => { + this.currentState.setOutOfBandMembers(result.memberEvents); + // now the members are loaded, start to track the e2e devices if needed + if (this._client.isRoomEncrypted(this.roomId)) { + this._client._crypto.trackRoomDevices(this.roomId); + } + return result.fromServer; + }).catch((err) => { + // allow retries on fail + this._membersPromise = null; + this.currentState.markOutOfBandMembersFailed(); + throw err; + }); + // update members in storage, but don't wait for it + inMemoryUpdate.then((fromServer) => { + if (fromServer) { + const oobMembers = this.currentState.getMembers() + .filter((m) => m.isOutOfBand()) + .map((m) => m.events.member.event); + console.log(`LL: telling store to write ${oobMembers.length}` + + ` members for room ${this.roomId}`); + const store = this._client.store; + return store.setOutOfBandMembers(this.roomId, oobMembers) + // swallow any IDB error as we don't want to fail + // because of this + .catch((err) => { + console.log("LL: storing OOB room members failed, oh well", + err); + }); + } + }).catch((err) => { + // as this is not awaited anywhere, + // at least show the error in the console + console.error(err); + }); + + this._membersPromise = inMemoryUpdate; + + return this._membersPromise; +}; + +/** + * Removes the lazily loaded members from storage if needed + */ +Room.prototype.clearLoadedMembersIfNeeded = async function() { + if (this._opts.lazyLoadMembers && this._membersPromise) { + await this.loadMembersIfNeeded(); + await this._client.store.clearOutOfBandMembers(this.roomId); + this.currentState.clearOutOfBandMembers(); + this._membersPromise = null; + } +}; + +/** + * called when sync receives this room in the leave section + * to do cleanup after leaving a room. Possibly called multiple times. + */ +Room.prototype._cleanupAfterLeaving = function() { + this.clearLoadedMembersIfNeeded().catch((err) => { + console.error(`error after clearing loaded members from ` + + `room ${this.roomId} after leaving`); + console.dir(err); + }); +}; /** * Reset the live timeline of all timelineSets, and start new ones. @@ -306,6 +586,26 @@ Room.prototype.setUnreadNotificationCount = function(type, count) { this._notificationCounts[type] = count; }; +Room.prototype.setSummary = function(summary) { + const heroes = summary["m.heroes"]; + const joinedCount = summary["m.joined_member_count"]; + const invitedCount = summary["m.invited_member_count"]; + if (Number.isInteger(joinedCount)) { + this.currentState.setJoinedMemberCount(joinedCount); + } + if (Number.isInteger(invitedCount)) { + this.currentState.setInvitedMemberCount(invitedCount); + } + if (Array.isArray(heroes)) { + // be cautious about trusting server values, + // and make sure heroes doesn't contain our own id + // just to be sure + this._summaryHeroes = heroes.filter((userId) => { + return userId !== this.myUserId; + }); + } +}; + /** * Whether to send encrypted messages to devices within this room. * @param {Boolean} value true to blacklist unverified devices, null @@ -430,11 +730,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline, * @return {RoomMember} The member or null. */ Room.prototype.getMember = function(userId) { - const member = this.currentState.members[userId]; - if (!member) { - return null; - } - return member; + return this.currentState.getMember(userId); }; /** @@ -445,6 +741,33 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline, return this.getMembersWithMembership("join"); }; +/** + * Returns the number of joined members in this room + * This method caches the result. + * This is a wrapper around the method of the same name in roomState, returning + * its result for the room's current state. + * @return {integer} The number of members in this room whose membership is 'join' + */ +Room.prototype.getJoinedMemberCount = function() { + return this.currentState.getJoinedMemberCount(); +}; + +/** + * Returns the number of invited members in this room + * @return {integer} The number of members in this room whose membership is 'invite' + */ +Room.prototype.getInvitedMemberCount = function() { + return this.currentState.getInvitedMemberCount(); +}; + +/** + * Returns the number of invited + joined members in this room + * @return {integer} The number of members in this room whose membership is 'invite' or 'join' + */ +Room.prototype.getInvitedAndJoinedMemberCount = function() { + return this.getInvitedMemberCount() + this.getJoinedMemberCount(); +}; + /** * Get a list of members with given membership state. * @param {string} membership The membership state. @@ -458,10 +781,11 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline, /** * Get a list of members we should be encrypting for in this room - * @return {RoomMember[]} A list of members who we should encrypt messages for - * in this room. + * @return {Promise} A list of members who + * we should encrypt messages for in this room. */ - Room.prototype.getEncryptionTargetMembers = function() { + Room.prototype.getEncryptionTargetMembers = async function() { + await this.loadMembersIfNeeded(); let members = this.getMembersWithMembership("join"); if (this.shouldEncryptForInvitedMembers()) { members = members.concat(this.getMembersWithMembership("invite")); @@ -675,6 +999,10 @@ Room.prototype.addPendingEvent = function(event, txnId) { this._txnToEvent[txnId] = event; if (this._opts.pendingEventOrdering == "detached") { + if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) { + console.warn("Setting event as NOT_SENT due to messages in the same state"); + event.status = EventStatus.NOT_SENT; + } this._pendingEventList.push(event); } else { for (let i = 0; i < this._timelineSets.length; i++) { @@ -839,7 +1167,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) { this.removeEvent(oldEventId); } - this.emit("Room.localEchoUpdated", event, this, event.getId(), oldStatus); + this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus); }; @@ -931,15 +1259,14 @@ Room.prototype.removeEvent = function(eventId) { * Recalculate various aspects of the room, including the room name and * room summary. Call this any time the room's current state is modified. * May fire "Room.name" if the room name is updated. - * @param {string} userId The client's user ID. * @fires module:client~MatrixClient#event:"Room.name" */ -Room.prototype.recalculate = function(userId) { +Room.prototype.recalculate = function() { // set fake stripped state events if this is an invite room so logic remains // consistent elsewhere. const self = this; const membershipEvent = this.currentState.getStateEvents( - "m.room.member", userId, + "m.room.member", this.myUserId, ); if (membershipEvent && membershipEvent.getContent().membership === "invite") { const strippedStateEvents = membershipEvent.event.invite_room_state || []; @@ -955,14 +1282,14 @@ Room.prototype.recalculate = function(userId) { content: strippedEvent.content, event_id: "$fake" + Date.now(), room_id: self.roomId, - user_id: userId, // technically a lie + user_id: self.myUserId, // technically a lie })]); } }); } const oldName = this.name; - this.name = calculateRoomName(this, userId); + this.name = calculateRoomName(this, this.myUserId); this.summary = new RoomSummary(this.roomId, { title: this.name, }); @@ -1143,7 +1470,7 @@ Room.prototype.addTags = function(event) { // } // XXX: do we need to deep copy here? - this.tags = event.getContent().tags; + this.tags = event.getContent().tags || {}; // XXX: we could do a deep-comparison to see if the tags have really // changed - but do we want to bother? @@ -1174,6 +1501,17 @@ Room.prototype.getAccountData = function(type) { return this.accountData[type]; }; + +/** + * Returns wheter the syncing user has permission to send a message in the room + * @return {boolean} true if the user should be permitted to send + * message events into the room. + */ +Room.prototype.maySendMessage = function() { + return this.getMyMembership() === 'join' && + this.currentState.maySendEvent('m.room.message', this.myUserId); +}; + /** * This is an internal method. Calculates the name of the room from the current * room state. @@ -1207,87 +1545,83 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { return alias; } - // get members that are NOT ourselves and are actually in the room. - const otherMembers = utils.filter(room.currentState.getMembers(), function(m) { - return ( - m.userId !== userId && m.membership !== "leave" && m.membership !== "ban" - ); - }); - const allMembers = utils.filter(room.currentState.getMembers(), function(m) { - return (m.membership !== "leave"); - }); - const myMemberEventArray = utils.filter(room.currentState.getMembers(), function(m) { - return (m.userId == userId); - }); - const myMemberEvent = ( - (myMemberEventArray.length && myMemberEventArray[0].events) ? - myMemberEventArray[0].events.member.event : undefined - ); + const joinedMemberCount = room.currentState.getJoinedMemberCount(); + const invitedMemberCount = room.currentState.getInvitedMemberCount(); + // -1 because these numbers include the syncing user + const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1; - // TODO: Localisation - if (myMemberEvent && myMemberEvent.content.membership == "invite") { - if (room.currentState.getMember(myMemberEvent.sender)) { - // extract who invited us to the room - return room.currentState.getMember( - myMemberEvent.sender, - ).name; - } else if (allMembers[0].events.member) { - // use the sender field from the invite event, although this only - // gets us the mxid - return myMemberEvent.sender; - } else { - return "Room Invite"; - } + // get members that are NOT ourselves and are actually in the room. + let otherNames = null; + if (room._summaryHeroes) { + // if we have a summary, the member state events + // should be in the room state + otherNames = room._summaryHeroes.map((userId) => { + const member = room.getMember(userId); + return member ? member.name : userId; + }); + } else { + let otherMembers = room.currentState.getMembers().filter((m) => { + return m.userId !== userId && + (m.membership === "invite" || m.membership === "join"); + }); + // make sure members have stable order + otherMembers.sort((a, b) => a.userId.localeCompare(b.userId)); + // only 5 first members, immitate _summaryHeroes + otherMembers = otherMembers.slice(0, 5); + otherNames = otherMembers.map((m) => m.name); } + if (inviteJoinCount) { + return memberNamesToRoomName(otherNames, inviteJoinCount); + } - if (otherMembers.length === 0) { - const leftMembers = utils.filter(room.currentState.getMembers(), function(m) { - return m.userId !== userId && m.membership === "leave"; - }); - if (allMembers.length === 1) { - // self-chat, peeked room with 1 participant, - // or inbound invite, or outbound 3PID invite. - if (allMembers[0].userId === userId) { - const thirdPartyInvites = - room.currentState.getStateEvents("m.room.third_party_invite"); - if (thirdPartyInvites && thirdPartyInvites.length > 0) { - let name = "Inviting " + - thirdPartyInvites[0].getContent().display_name; - if (thirdPartyInvites.length > 1) { - if (thirdPartyInvites.length == 2) { - name += " and " + - thirdPartyInvites[1].getContent().display_name; - } else { - name += " and " + - thirdPartyInvites.length + " others"; - } - } - return name; - } else if (leftMembers.length === 1) { - // if it was a chat with one person who's now left, it's still - // notionally a chat with them - return leftMembers[0].name; - } else { - return "Empty room"; - } - } else { - return allMembers[0].name; - } - } else { - // there really isn't anyone in this room... - return "Empty room"; + const myMembership = room.getMyMembership(); + // if I have created a room and invited people throuh + // 3rd party invites + if (myMembership == 'join') { + const thirdPartyInvites = + room.currentState.getStateEvents("m.room.third_party_invite"); + + if (thirdPartyInvites && thirdPartyInvites.length) { + const thirdPartyNames = thirdPartyInvites.map((i) => { + return i.getContent().display_name; + }); + + return `Inviting ${memberNamesToRoomName(thirdPartyNames)}`; } - } else if (otherMembers.length === 1) { - return otherMembers[0].name; - } else if (otherMembers.length === 2) { - return ( - otherMembers[0].name + " and " + otherMembers[1].name - ); + } + // let's try to figure out who was here before + let leftNames = otherNames; + // if we didn't have heroes, try finding them in the room state + if(!leftNames.length) { + leftNames = room.currentState.getMembers().filter((m) => { + return m.userId !== userId && + m.membership !== "invite" && + m.membership !== "join"; + }).map((m) => m.name); + } + if(leftNames.length) { + return `Empty room (was ${memberNamesToRoomName(leftNames)})`; } else { - return ( - otherMembers[0].name + " and " + (otherMembers.length - 1) + " others" - ); + return "Empty room"; + } +} + +function memberNamesToRoomName(names, count = (names.length + 1)) { + const countWithoutMe = count - 1; + if (!names.length) { + return "Empty room"; + } else if (names.length === 1 && countWithoutMe <= 1) { + return names[0]; + } else if (names.length === 2 && countWithoutMe <= 2) { + return `${names[0]} and ${names[1]}`; + } else { + const plural = countWithoutMe > 1; + if (plural) { + return `${names[0]} and ${countWithoutMe} others`; + } else { + return `${names[0]} and 1 other`; + } } } diff --git a/src/models/user.js b/src/models/user.js index dbfaeb791..ec2f495af 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -39,6 +39,9 @@ limitations under the License. * when a user was last active. * @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be * an approximation and that the user should be seen as active 'now' + * @prop {string} _unstable_statusMessage The status message for the user, if known. This is + * different from the presenceStatusMsg in that this is not tied to + * the user's presence, and should be represented differently. * @prop {Object} events The events describing this user. * @prop {MatrixEvent} events.presence The m.presence event for this user. */ @@ -46,6 +49,7 @@ function User(userId) { this.userId = userId; this.presence = "offline"; this.presenceStatusMsg = null; + this._unstable_statusMessage = ""; this.displayName = userId; this.rawDisplayName = userId; this.avatarUrl = null; @@ -179,6 +183,16 @@ User.prototype.getLastActiveTs = function() { return this.lastPresenceTs - this.lastActiveAgo; }; +/** + * Manually set the user's status message. + * @param {MatrixEvent} event The im.vector.user_status event. + */ +User.prototype._unstable_updateStatusMessage = function(event) { + if (!event.getContent()) this._unstable_statusMessage = ""; + else this._unstable_statusMessage = event.getContent()["status"]; + this._updateModifiedTime(); +}; + /** * The User class. */ diff --git a/src/pushprocessor.js b/src/pushprocessor.js index 1fdbdfe84..712c45399 100644 --- a/src/pushprocessor.js +++ b/src/pushprocessor.js @@ -14,6 +14,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +import {escapeRegExp, globToRegexp} from "./utils"; + /** * @module pushprocessor */ @@ -26,10 +29,6 @@ const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride' * @param {Object} client The Matrix client object to use */ function PushProcessor(client) { - const escapeRegExp = function(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - }; - const cachedGlobToRegex = { // $glob: RegExp, }; @@ -244,22 +243,6 @@ function PushProcessor(client) { return cachedGlobToRegex[glob]; }; - const globToRegexp = function(glob) { - // From - // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 - // Because micromatch is about 130KB with dependencies, - // and minimatch is not much better. - let pat = escapeRegExp(glob); - pat = pat.replace(/\\\*/g, '.*'); - pat = pat.replace(/\?/g, '.'); - pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) { - const first = p1 && '^' || ''; - const second = p2.replace(/\\\-/, '-'); - return '[' + first + second + ']'; - }); - return pat; - }; - const valueForDottedKey = function(key, ev) { const parts = key.split('.'); let val; diff --git a/src/randomstring.js b/src/randomstring.js new file mode 100644 index 000000000..7ebe4ed78 --- /dev/null +++ b/src/randomstring.js @@ -0,0 +1,26 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export function randomString(len) { + let ret = ""; + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (let i = 0; i < len; ++i) { + ret += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return ret; +} diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index a928e1aa6..e3ae1c1ae 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -19,7 +19,7 @@ import Promise from 'bluebird'; import SyncAccumulator from "../sync-accumulator"; import utils from "../utils"; -const VERSION = 1; +const VERSION = 3; function createDatabase(db) { // Make user store, clobber based on user ID. (userId property of User objects) @@ -33,6 +33,20 @@ function createDatabase(db) { db.createObjectStore("sync", { keyPath: ["clobber"] }); } +function upgradeSchemaV2(db) { + const oobMembersStore = db.createObjectStore( + "oob_membership_events", { + keyPath: ["room_id", "state_key"], + }); + oobMembersStore.createIndex("room", "room_id"); +} + +function upgradeSchemaV3(db) { + db.createObjectStore("client_options", + { keyPath: ["clobber"]}); +} + + /** * Helper method to collect results from a Cursor and promiseify it. * @param {ObjectStore|Index} store The store to perform openCursor on. @@ -63,28 +77,39 @@ function selectQuery(store, keyRange, resultMapper) { }); } -function promiseifyTxn(txn) { +function txnAsPromise(txn) { return new Promise((resolve, reject) => { txn.oncomplete = function(event) { resolve(event); }; txn.onerror = function(event) { - reject(event); + reject(event.target.error); }; }); } -function promiseifyRequest(req) { +function reqAsEventPromise(req) { return new Promise((resolve, reject) => { req.onsuccess = function(event) { resolve(event); }; req.onerror = function(event) { - reject(event); + reject(event.target.error); }; }); } +function reqAsPromise(req) { + return new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req); + req.onerror = (err) => reject(err); + }); +} + +function reqAsCursorPromise(req) { + return reqAsEventPromise(req).then((event) => event.target.result); +} + /** * Does the actual reading from and writing to the indexeddb * @@ -104,6 +129,7 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend( this.db = null; this._disconnected = true; this._syncAccumulator = new SyncAccumulator(); + this._isNewlyCreated = false; }; @@ -134,8 +160,15 @@ LocalIndexedDBStoreBackend.prototype = { `LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`, ); if (oldVersion < 1) { // The database did not previously exist. + this._isNewlyCreated = true; createDatabase(db); } + if (oldVersion < 2) { + upgradeSchemaV2(db); + } + if (oldVersion < 3) { + upgradeSchemaV3(db); + } // Expand as needed. }; @@ -148,7 +181,7 @@ LocalIndexedDBStoreBackend.prototype = { console.log( `LocalIndexedDBStoreBackend.connect: awaiting connection...`, ); - return promiseifyRequest(req).then((ev) => { + return reqAsEventPromise(req).then((ev) => { console.log( `LocalIndexedDBStoreBackend.connect: connected`, ); @@ -163,6 +196,10 @@ LocalIndexedDBStoreBackend.prototype = { return this._init(); }); }, + /** @return {bool} whether or not the database was newly created in this session. */ + isNewlyCreated: function() { + return Promise.resolve(this._isNewlyCreated); + }, /** * Having connected, load initial data from the database and prepare for use @@ -187,6 +224,124 @@ LocalIndexedDBStoreBackend.prototype = { }); }, + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @param {string} roomId + * @returns {Promise} the events, potentially an empty array if OOB loading didn't yield any new members + * @returns {null} in case the members for this room haven't been stored yet + */ + getOutOfBandMembers: function(roomId) { + return new Promise((resolve, reject) =>{ + const tx = this.db.transaction(["oob_membership_events"], "readonly"); + const store = tx.objectStore("oob_membership_events"); + const roomIndex = store.index("room"); + const range = IDBKeyRange.only(roomId); + const request = roomIndex.openCursor(range); + + const membershipEvents = []; + // did we encounter the oob_written marker object + // amongst the results? That means OOB member + // loading already happened for this room + // but there were no members to persist as they + // were all known already + let oobWritten = false; + + request.onsuccess = (event) => { + const cursor = event.target.result; + if (!cursor) { + // Unknown room + if (!membershipEvents.length && !oobWritten) { + return resolve(null); + } + return resolve(membershipEvents); + } + const record = cursor.value; + if (record.oob_written) { + oobWritten = true; + } else { + membershipEvents.push(record); + } + cursor.continue(); + }; + request.onerror = (err) => { + reject(err); + }; + }).then((events) => { + console.log(`LL: got ${events && events.length}` + + ` membershipEvents from storage for room ${roomId} ...`); + return events; + }); + }, + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param {string} roomId + * @param {event[]} membershipEvents the membership events to store + */ + setOutOfBandMembers: async function(roomId, membershipEvents) { + console.log(`LL: backend about to store ${membershipEvents.length}` + + ` members for ${roomId}`); + const tx = this.db.transaction(["oob_membership_events"], "readwrite"); + const store = tx.objectStore("oob_membership_events"); + membershipEvents.forEach((e) => { + store.put(e); + }); + // aside from all the events, we also write a marker object to the store + // to mark the fact that OOB members have been written for this room. + // It's possible that 0 members need to be written as all where previously know + // but we still need to know whether to return null or [] from getOutOfBandMembers + // where null means out of band members haven't been stored yet for this room + const markerObject = { + room_id: roomId, + oob_written: true, + state_key: 0, + }; + store.put(markerObject); + await txnAsPromise(tx); + console.log(`LL: backend done storing for ${roomId}!`); + }, + + clearOutOfBandMembers: async function(roomId) { + // the approach to delete all members for a room + // is to get the min and max state key from the index + // for that room, and then delete between those + // keys in the store. + // this should be way faster than deleting every member + // individually for a large room. + const readTx = this.db.transaction( + ["oob_membership_events"], + "readonly"); + const store = readTx.objectStore("oob_membership_events"); + const roomIndex = store.index("room"); + const roomRange = IDBKeyRange.only(roomId); + + const minStateKeyProm = reqAsCursorPromise( + roomIndex.openKeyCursor(roomRange, "next"), + ).then((cursor) => cursor && cursor.primaryKey[1]); + const maxStateKeyProm = reqAsCursorPromise( + roomIndex.openKeyCursor(roomRange, "prev"), + ).then((cursor) => cursor && cursor.primaryKey[1]); + const [minStateKey, maxStateKey] = await Promise.all( + [minStateKeyProm, maxStateKeyProm]); + + const writeTx = this.db.transaction( + ["oob_membership_events"], + "readwrite"); + const writeStore = writeTx.objectStore("oob_membership_events"); + const membersKeyRange = IDBKeyRange.bound( + [roomId, minStateKey], + [roomId, maxStateKey], + ); + + console.log(`LL: Deleting all users + marker in storage for ` + + `room ${roomId}, with key range:`, + [roomId, minStateKey], [roomId, maxStateKey]); + await reqAsPromise(writeStore.delete(membersKeyRange)); + }, + /** * Clear the entire database. This should be used when logging out of a client * to prevent mixing data between accounts. @@ -284,7 +439,7 @@ LocalIndexedDBStoreBackend.prototype = { roomsData: roomsData, groupsData: groupsData, }); // put == UPSERT - return promiseifyTxn(txn); + return txnAsPromise(txn); }); }, @@ -301,7 +456,7 @@ LocalIndexedDBStoreBackend.prototype = { for (let i = 0; i < accountData.length; i++) { store.put(accountData[i]); // put == UPSERT } - return promiseifyTxn(txn); + return txnAsPromise(txn); }); }, @@ -323,7 +478,7 @@ LocalIndexedDBStoreBackend.prototype = { event: tuple[1], }); // put == UPSERT } - return promiseifyTxn(txn); + return txnAsPromise(txn); }); }, @@ -389,6 +544,28 @@ LocalIndexedDBStoreBackend.prototype = { }); }); }, + + getClientOptions: function() { + return Promise.resolve().then(() => { + const txn = this.db.transaction(["client_options"], "readonly"); + const store = txn.objectStore("client_options"); + return selectQuery(store, undefined, (cursor) => { + if (cursor.value && cursor.value && cursor.value.options) { + return cursor.value.options; + } + }).then((results) => results[0]); + }); + }, + + storeClientOptions: async function(options) { + const txn = this.db.transaction(["client_options"], "readwrite"); + const store = txn.objectStore("client_options"); + store.put({ + clobber: "-", // constant key so will always clobber + options: options, + }); // put == UPSERT + await txnAsPromise(txn); + }, }; export default LocalIndexedDBStoreBackend; diff --git a/src/store/indexeddb-remote-backend.js b/src/store/indexeddb-remote-backend.js index 2221633e4..035a821ff 100644 --- a/src/store/indexeddb-remote-backend.js +++ b/src/store/indexeddb-remote-backend.js @@ -65,7 +65,10 @@ RemoteIndexedDBStoreBackend.prototype = { clearDatabase: function() { return this._ensureStarted().then(() => this._doCmd('clearDatabase')); }, - + /** @return {Promise} whether or not the database was newly created in this session. */ + isNewlyCreated: function() { + return this._doCmd('isNewlyCreated'); + }, /** * @return {Promise} Resolves with a sync response to restore the * client state to where it was at the last save, or null if there @@ -87,6 +90,40 @@ RemoteIndexedDBStoreBackend.prototype = { return this._doCmd('syncToDatabase', [users]); }, + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @param {string} roomId + * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members + * @returns {null} in case the members for this room haven't been stored yet + */ + getOutOfBandMembers: function(roomId) { + return this._doCmd('getOutOfBandMembers', [roomId]); + }, + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param {string} roomId + * @param {event[]} membershipEvents the membership events to store + * @returns {Promise} when all members have been stored + */ + setOutOfBandMembers: function(roomId, membershipEvents) { + return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]); + }, + + clearOutOfBandMembers: function(roomId) { + return this._doCmd('clearOutOfBandMembers', [roomId]); + }, + + getClientOptions: function() { + return this._doCmd('getClientOptions'); + }, + + storeClientOptions: function(options) { + return this._doCmd('storeClientOptions', [options]); + }, /** * Load all user presence events from the database. This is not cached. @@ -147,7 +184,9 @@ RemoteIndexedDBStoreBackend.prototype = { if (msg.command == 'cmd_success') { def.resolve(msg.result); } else { - def.reject(msg.error); + const error = new Error(msg.error.message); + error.name = msg.error.name; + def.reject(error); } } else { console.warn("Unrecognised message from worker: " + msg); diff --git a/src/store/indexeddb-store-worker.js b/src/store/indexeddb-store-worker.js index d32df7274..4e66f51aa 100644 --- a/src/store/indexeddb-store-worker.js +++ b/src/store/indexeddb-store-worker.js @@ -67,6 +67,9 @@ class IndexedDBStoreWorker { case 'connect': prom = this.backend.connect(); break; + case 'isNewlyCreated': + prom = this.backend.isNewlyCreated(); + break; case 'clearDatabase': prom = this.backend.clearDatabase().then((result) => { // This returns special classes which can't be cloned @@ -92,10 +95,25 @@ class IndexedDBStoreWorker { case 'getNextBatchToken': prom = this.backend.getNextBatchToken(); break; + case 'getOutOfBandMembers': + prom = this.backend.getOutOfBandMembers(msg.args[0]); + break; + case 'clearOutOfBandMembers': + prom = this.backend.clearOutOfBandMembers(msg.args[0]); + break; + case 'setOutOfBandMembers': + prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]); + break; + case 'getClientOptions': + prom = this.backend.getClientOptions(); + break; + case 'storeClientOptions': + prom = this.backend.storeClientOptions(msg.args[0]); + break; } if (prom === undefined) { - postMessage({ + this.postMessage({ command: 'cmd_fail', seq: msg.seq, // Can't be an Error because they're not structured cloneable @@ -117,7 +135,10 @@ class IndexedDBStoreWorker { command: 'cmd_fail', seq: msg.seq, // Just send a string because Error objects aren't cloneable - error: "Error running command", + error: { + message: err.message, + name: err.name, + }, }); }); } diff --git a/src/store/indexeddb.js b/src/store/indexeddb.js index d47e35544..b8f47ad89 100644 --- a/src/store/indexeddb.js +++ b/src/store/indexeddb.js @@ -146,6 +146,11 @@ IndexedDBStore.prototype.getSavedSync = function() { return this.backend.getSavedSync(); }; +/** @return {Promise} whether or not the database was newly created in this session. */ +IndexedDBStore.prototype.isNewlyCreated = function() { + return this.backend.isNewlyCreated(); +}; + /** * @return {Promise} If there is a saved sync, the nextBatch token * for this sync, otherwise null. @@ -219,4 +224,39 @@ IndexedDBStore.prototype.setSyncData = function(syncData) { return this.backend.setSyncData(syncData); }; +/** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @param {string} roomId + * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members + * @returns {null} in case the members for this room haven't been stored yet + */ +IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) { + return this.backend.getOutOfBandMembers(roomId); +}; + +/** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param {string} roomId + * @param {event[]} membershipEvents the membership events to store + * @returns {Promise} when all members have been stored + */ +IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents) { + return this.backend.setOutOfBandMembers(roomId, membershipEvents); +}; + +IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) { + return this.backend.clearOutOfBandMembers(roomId); +}; + +IndexedDBStore.prototype.getClientOptions = function() { + return this.backend.getClientOptions(); +}; + +IndexedDBStore.prototype.storeClientOptions = function(options) { + return this.backend.storeClientOptions(options); +}; + module.exports.IndexedDBStore = IndexedDBStore; diff --git a/src/store/memory.js b/src/store/memory.js index 4f2f7c754..006742223 100644 --- a/src/store/memory.js +++ b/src/store/memory.js @@ -52,6 +52,10 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) { // type : content }; this.localStorage = opts.localStorage; + this._oobMembers = { + // roomId: [member events] + }; + this._clientOptions = {}; }; module.exports.MatrixInMemoryStore.prototype = { @@ -64,6 +68,10 @@ module.exports.MatrixInMemoryStore.prototype = { return this.syncToken; }, + /** @return {Promise} whether or not the database was newly created in this session. */ + isNewlyCreated: function() { + return Promise.resolve(true); + }, /** * Set the token to stream from. @@ -377,4 +385,35 @@ module.exports.MatrixInMemoryStore.prototype = { }; return Promise.resolve(); }, + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @param {string} roomId + * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members + * @returns {null} in case the members for this room haven't been stored yet + */ + getOutOfBandMembers: function(roomId) { + return Promise.resolve(this._oobMembers[roomId] || null); + }, + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param {string} roomId + * @param {event[]} membershipEvents the membership events to store + * @returns {Promise} when all members have been stored + */ + setOutOfBandMembers: function(roomId, membershipEvents) { + this._oobMembers[roomId] = membershipEvents; + return Promise.resolve(); + }, + + getClientOptions: function() { + return Promise.resolve(this._clientOptions); + }, + + storeClientOptions: function(options) { + this._clientOptions = Object.assign({}, options); + return Promise.resolve(); + }, }; diff --git a/src/store/stub.js b/src/store/stub.js index f41e0c5e0..f09dad6d7 100644 --- a/src/store/stub.js +++ b/src/store/stub.js @@ -32,6 +32,11 @@ function StubStore() { StubStore.prototype = { + /** @return {Promise} whether or not the database was newly created in this session. */ + isNewlyCreated: function() { + return Promise.resolve(true); + }, + /** * Get the sync token. * @return {string} @@ -264,6 +269,26 @@ StubStore.prototype = { deleteAllData: function() { return Promise.resolve(); }, + + getOutOfBandMembers: function() { + return Promise.resolve(null); + }, + + setOutOfBandMembers: function() { + return Promise.resolve(); + }, + + clearOutOfBandMembers: function() { + return Promise.resolve(); + }, + + getClientOptions: function() { + return Promise.resolve(); + }, + + storeClientOptions: function() { + return Promise.resolve(); + }, }; /** Stub Store class. */ diff --git a/src/sync-accumulator.js b/src/sync-accumulator.js index 7369c9b8a..634275cb9 100644 --- a/src/sync-accumulator.js +++ b/src/sync-accumulator.js @@ -63,6 +63,11 @@ class SyncAccumulator { // { event: $event, token: null|token }, // ... // ], + // _summary: { + // m.heroes: [ $user_id ], + // m.joined_member_count: $count, + // m.invited_member_count: $count + // }, // _accountData: { $event_type: json }, // _unreadNotifications: { ... unread_notifications JSON ... }, // _readReceipts: { $user_id: { data: $json, eventId: $event_id }} @@ -242,6 +247,7 @@ class SyncAccumulator { _timeline: [], _accountData: Object.create(null), _unreadNotifications: {}, + _summary: {}, _readReceipts: {}, }; } @@ -258,6 +264,17 @@ class SyncAccumulator { if (data.unread_notifications) { currentData._unreadNotifications = data.unread_notifications; } + if (data.summary) { + const HEROES_KEY = "m.heroes"; + const INVITED_COUNT_KEY = "m.invited_member_count"; + const JOINED_COUNT_KEY = "m.joined_member_count"; + + const acc = currentData._summary; + const sum = data.summary; + acc[HEROES_KEY] = sum[HEROES_KEY] || acc[HEROES_KEY]; + acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] || acc[JOINED_COUNT_KEY]; + acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] || acc[INVITED_COUNT_KEY]; + } if (data.ephemeral && data.ephemeral.events) { data.ephemeral.events.forEach((e) => { @@ -428,6 +445,7 @@ class SyncAccumulator { prev_batch: null, }, unread_notifications: roomData._unreadNotifications, + summary: roomData._summary, }; // Add account data Object.keys(roomData._accountData).forEach((evType) => { diff --git a/src/sync.js b/src/sync.js index 2636460b9..f4ea0bf88 100644 --- a/src/sync.js +++ b/src/sync.js @@ -33,6 +33,8 @@ const utils = require("./utils"); const Filter = require("./filter"); const EventTimeline = require("./models/event-timeline"); +import {InvalidStoreError} from './errors'; + const DEBUG = true; // /sync requests allow you to set a timeout= but the request may continue @@ -93,12 +95,14 @@ function SyncApi(client, opts) { this._peekRoomId = null; this._currentSyncRequest = null; this._syncState = null; + this._syncStateData = null; // additional data (eg. error object for failed sync) this._catchingUp = false; this._running = false; this._keepAliveTimer = null; this._connectionReturnedDefer = null; this._notifEvents = []; // accumulator of sync events in the current sync response this._failedSyncCount = 0; // Number of consecutive failed /sync requests + this._storeIsInvalid = false; // flag set if the store needs to be cleared before we can start if (client.getNotifTimelineSet()) { client.reEmitter.reEmit(client.getNotifTimelineSet(), @@ -112,7 +116,8 @@ function SyncApi(client, opts) { */ SyncApi.prototype.createRoom = function(roomId) { const client = this.client; - const room = new Room(roomId, { + const room = new Room(roomId, client, client.getUserId(), { + lazyLoadMembers: this.opts.lazyLoadMembers, pendingEventOrdering: this.opts.pendingEventOrdering, timelineSupport: client.timelineSupport, }); @@ -121,6 +126,7 @@ SyncApi.prototype.createRoom = function(roomId) { "Room.timelineReset", "Room.localEchoUpdated", "Room.accountData", + "Room.myMembership", ]); this._registerStateListeners(room); return room; @@ -231,7 +237,7 @@ SyncApi.prototype.syncLeftRooms = function() { self._processRoomEvents(room, stateEvents, timelineEvents); - room.recalculate(client.credentials.userId); + room.recalculate(); client.store.storeRoom(room); client.emit("Room", room); @@ -302,7 +308,7 @@ SyncApi.prototype.peek = function(roomId) { peekRoom.currentState.setStateEvents(stateEvents); self._resolveInvites(peekRoom); - peekRoom.recalculate(self.client.credentials.userId); + peekRoom.recalculate(); // roll backwards to diverge old state. addEventsToTimeline // will overwrite the pagination token, so make sure it overwrites @@ -396,6 +402,18 @@ SyncApi.prototype.getSyncState = function() { return this._syncState; }; +/** + * Returns the additional data object associated with + * the current sync state, or null if there is no + * such data. + * Sync errors, if available, are put in the 'error' key of + * this object. + * @return {?Object} + */ +SyncApi.prototype.getSyncStateData = function() { + return this._syncStateData; +}; + SyncApi.prototype.recoverFromSyncStartupError = async function(savedSyncPromise, err) { // Wait for the saved sync to complete - we send the pushrules and filter requests // before the saved sync has finished so they can run in parallel, but only process @@ -407,6 +425,26 @@ SyncApi.prototype.recoverFromSyncStartupError = async function(savedSyncPromise, await keepaliveProm; }; +/** + * Is the lazy loading option different than in previous session? + * @param {bool} lazyLoadMembers current options for lazy loading + * @return {bool} whether or not the option has changed compared to the previous session */ +SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) { + lazyLoadMembers = !!lazyLoadMembers; + // assume it was turned off before + // if we don't know any better + let lazyLoadMembersBefore = false; + const isStoreNewlyCreated = await this.client.store.isNewlyCreated(); + if (!isStoreNewlyCreated) { + const prevClientOptions = await this.client.store.getClientOptions(); + if (prevClientOptions) { + lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers; + } + return lazyLoadMembersBefore !== lazyLoadMembers; + } + return false; +}; + /** * Main entry point */ @@ -429,6 +467,8 @@ SyncApi.prototype.sync = function() { // 1) We need to get push rules so we can check if events should bing as we get // them from /sync. // 2) We need to get/create a filter which we can use for /sync. + // 3) We need to check the lazy loading option matches what was used in the + // stored sync. If it doesn't, we can't use the stored sync. async function getPushRules() { try { @@ -443,9 +483,47 @@ SyncApi.prototype.sync = function() { getPushRules(); return; } - getFilter(); // Now get the filter and start syncing + checkLazyLoadStatus(); // advance to the next stage } + const checkLazyLoadStatus = async () => { + if (this.opts.lazyLoadMembers && client.isGuest()) { + this.opts.lazyLoadMembers = false; + } + if (this.opts.lazyLoadMembers) { + const supported = await client.doesServerSupportLazyLoading(); + if (supported) { + this.opts.filter = await client.createFilter( + Filter.LAZY_LOADING_SYNC_FILTER, + ); + } else { + console.log("LL: lazy loading requested but not supported " + + "by server, so disabling"); + this.opts.lazyLoadMembers = false; + } + } + // need to vape the store when enabling LL and wasn't enabled before + const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers); + if (shouldClear) { + this._storeIsInvalid = true; + const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; + const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers); + this._updateSyncState("ERROR", { error }); + // bail out of the sync loop now: the app needs to respond to this error. + // we leave the state as 'ERROR' which isn't great since this normally means + // we're retrying. The client must be stopped before clearing the stores anyway + // so the app should stop the client, clear the store and start it again. + console.warn("InvalidStoreError: store is not usable: stopping sync."); + return; + } + if (this.opts.lazyLoadMembers && this.opts.crypto) { + this.opts.crypto.enableLazyLoading(); + } + await this.client._storeClientOptions(); + + getFilter(); // Now get the filter and start syncing + }; + async function getFilter() { let filter; if (self.opts.filter) { @@ -573,7 +651,12 @@ SyncApi.prototype._syncFromCache = async function(savedSync) { console.error("Error processing cached sync", e.stack || e); } - this._updateSyncState("PREPARED", syncEventData); + // Don't emit a prepared if we've bailed because the store is invalid: + // in this case the client will not be usable until stopped & restarted + // so this would be useless and misleading. + if (!this._storeIsInvalid) { + this._updateSyncState("PREPARED", syncEventData); + } }; /** @@ -763,9 +846,20 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) { // fails, since long lived HTTP connections will // go away sometimes and we shouldn't treat this as // erroneous. We set the state to 'reconnecting' - // instead, so that clients can onserve this state + // instead, so that clients can observe this state // if they wish. - this._startKeepAlives().then(() => { + this._startKeepAlives().then((connDidFail) => { + // Only emit CATCHUP if we detected a connectivity error: if we didn't, + // it's quite likely the sync will fail again for the same reason and we + // want to stay in ERROR rather than keep flip-flopping between ERROR + // and CATCHUP. + if (connDidFail && this.getSyncState() === 'ERROR') { + this._updateSyncState("CATCHUP", { + oldSyncToken: null, + nextSyncToken: null, + catchingUp: true, + }); + } this._sync(syncOptions); }); @@ -774,6 +868,7 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) { this._updateSyncState( this._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? "ERROR" : "RECONNECTING", + { error: err }, ); }; @@ -809,6 +904,11 @@ SyncApi.prototype._processSyncResponse = async function( // state: { events: [] }, // timeline: { events: [], prev_batch: $token, limited: true }, // ephemeral: { events: [] }, + // summary: { + // m.heroes: [ $user_id ], + // m.joined_member_count: $count, + // m.invited_member_count: $count + // }, // account_data: { events: [] }, // unread_notifications: { // highlight_count: 0, @@ -947,9 +1047,11 @@ SyncApi.prototype._processSyncResponse = async function( const room = inviteObj.room; const stateEvents = self._mapSyncEventsFormat(inviteObj.invite_state, room); + + room.updateMyMembership("invite"); self._processRoomEvents(room, stateEvents); if (inviteObj.isBrandNewRoom) { - room.recalculate(client.credentials.userId); + room.recalculate(); client.store.storeRoom(room); client.emit("Room", room); } @@ -976,6 +1078,8 @@ SyncApi.prototype._processSyncResponse = async function( ); } + room.updateMyMembership("join"); + joinObj.timeline = joinObj.timeline || {}; if (joinObj.isBrandNewRoom) { @@ -1040,6 +1144,13 @@ SyncApi.prototype._processSyncResponse = async function( self._processRoomEvents(room, stateEvents, timelineEvents); + // set summary after processing events, + // because it will trigger a name calculation + // which needs the room state to be up to date + if (joinObj.summary) { + room.setSummary(joinObj.summary); + } + // XXX: should we be adding ephemeralEvents to the timeline? // It feels like that for symmetry with room.addAccountData() // there should be a room.addEphemeralEvents() or similar. @@ -1048,7 +1159,7 @@ SyncApi.prototype._processSyncResponse = async function( // we deliberately don't add accountData to the timeline room.addAccountData(accountDataEvents); - room.recalculate(client.credentials.userId); + room.recalculate(); if (joinObj.isBrandNewRoom) { client.store.storeRoom(room); client.emit("Room", room); @@ -1061,6 +1172,16 @@ SyncApi.prototype._processSyncResponse = async function( if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { await self.opts.crypto.onCryptoEvent(e); } + if (e.isState() && e.getType() === "im.vector.user_status") { + let user = client.store.getUser(e.getStateKey()); + if (user) { + user._unstable_updateStatusMessage(e); + } else { + user = createNewUser(client, e.getStateKey()); + user._unstable_updateStatusMessage(e); + client.store.storeUser(user); + } + } } await Promise.mapSeries(stateEvents, processRoomEvent); @@ -1083,10 +1204,12 @@ SyncApi.prototype._processSyncResponse = async function( const accountDataEvents = self._mapSyncEventsFormat(leaveObj.account_data); + room.updateMyMembership("leave"); + self._processRoomEvents(room, stateEvents, timelineEvents); room.addAccountData(accountDataEvents); - room.recalculate(client.credentials.userId); + room.recalculate(); if (leaveObj.isBrandNewRoom) { client.store.storeRoom(room); client.emit("Room", room); @@ -1175,13 +1298,16 @@ SyncApi.prototype._startKeepAlives = function(delay) { * * On failure, schedules a call back to itself. On success, resolves * this._connectionReturnedDefer. + * + * @param {bool} connDidFail True if a connectivity failure has been detected. Optional. */ -SyncApi.prototype._pokeKeepAlive = function() { +SyncApi.prototype._pokeKeepAlive = function(connDidFail) { + if (connDidFail === undefined) connDidFail = false; const self = this; function success() { clearTimeout(self._keepAliveTimer); if (self._connectionReturnedDefer) { - self._connectionReturnedDefer.resolve(); + self._connectionReturnedDefer.resolve(connDidFail); self._connectionReturnedDefer = null; } } @@ -1198,7 +1324,7 @@ SyncApi.prototype._pokeKeepAlive = function() { ).done(function() { success(); }, function(err) { - if (err.httpStatus == 400) { + if (err.httpStatus == 400 || err.httpStatus == 404) { // treat this as a success because the server probably just doesn't // support /versions: point is, we're getting a response. // We wait a short time though, just in case somehow the server @@ -1206,8 +1332,9 @@ SyncApi.prototype._pokeKeepAlive = function() { // responses fail, this will mean we don't hammer in a loop. self._keepAliveTimer = setTimeout(success, 2000); } else { + connDidFail = true; self._keepAliveTimer = setTimeout( - self._pokeKeepAlive.bind(self), + self._pokeKeepAlive.bind(self, connDidFail), 5000 + Math.floor(Math.random() * 5000), ); // A keepalive has failed, so we emit the @@ -1215,7 +1342,7 @@ SyncApi.prototype._pokeKeepAlive = function() { // first failure). // Note we do this after setting the timer: // this lets the unit tests advance the mock - // clock when the get the error. + // clock when they get the error. self._updateSyncState("ERROR", { error: err }); } }); @@ -1376,7 +1503,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList, // a recalculation (like m.room.name) we won't recalculate until we've // finished adding all the events, which will cause the notification to have // the old room name rather than the new one. - room.recalculate(this.client.credentials.userId); + room.recalculate(); // If the timeline wasn't empty, we process the state events here: they're // defined as updates to the state before the start of the timeline, so this @@ -1451,6 +1578,7 @@ SyncApi.prototype._getGuestFilter = function() { SyncApi.prototype._updateSyncState = function(newState, data) { const old = this._syncState; this._syncState = newState; + this._syncStateData = data; this.client.emit("sync", this._syncState, old, data); }; diff --git a/src/utils.js b/src/utils.js index 0f05d6eb3..265ce218e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -675,3 +675,27 @@ module.exports.removeHiddenChars = function(str) { return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, '')); }; const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g; + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} +module.exports.escapeRegExp = escapeRegExp; + +module.exports.globToRegexp = function(glob, extended) { + extended = typeof(extended) === 'boolean' ? extended : true; + // From + // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 + // Because micromatch is about 130KB with dependencies, + // and minimatch is not much better. + let pat = escapeRegExp(glob); + pat = pat.replace(/\\\*/g, '.*'); + pat = pat.replace(/\?/g, '.'); + if (extended) { + pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) { + const first = p1 && '^' || ''; + const second = p2.replace(/\\\-/, '-'); + return '[' + first + second + ']'; + }); + } + return pat; +}; diff --git a/travis.sh b/travis.sh index 68d915def..ddf4a790e 100755 --- a/travis.sh +++ b/travis.sh @@ -5,7 +5,7 @@ set -ex npm run lint # install Olm so that we can run the crypto tests. -npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz +npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz npm run test