Merge pull request #498 from RickCarlino/fix_auth_errors_II

(WIP) Fix more auth issues, refactorings.
This commit is contained in:
Rick Carlino 2017-10-12 19:46:40 -05:00 committed by GitHub
commit 34799c954e
23 changed files with 229 additions and 194 deletions

View file

@ -12,7 +12,8 @@ module CeleryScriptSettingsBag
move_relative write_pin read_pin send_message
factory_reset execute_script set_user_env wait
add_point install_farmware update_farmware zero
remove_farmware take_photo data_update find_home)
remove_farmware take_photo data_update find_home
install_first_party_farmware)
ALLOWED_PACKAGES = %w(farmbot_os arduino_firmware)
ALLOWED_CHAGES = %w(add remove update)
RESOURCE_NAME = %w(images plants regimens peripherals

View file

@ -8,8 +8,7 @@ conf.output = {
// must match config.webpack.output_dir
path: path.join(__dirname, '..', 'public', 'webpack'),
publicPath: '//localhost:' + devServerPort + '/webpack/',
filename: '[name].js',
watch: true
filename: '[name].js'
};
conf.devServer = {

View file

@ -13,7 +13,7 @@
"start": "echo '===We use `rails api:start` now.==='",
"heroku-postbuild": "webpack --config=./config/webpack.prod.js",
"dev": "echo '===We use `rails api:start` now.==='",
"webpack": "./node_modules/.bin/webpack-dev-server --config config/webpack.config.js --host 0.0.0.0",
"webpack": "./node_modules/.bin/webpack-dev-server --config config/webpack.dev.js --host 0.0.0.0",
"test": "jest --coverage --no-cache",
"typecheck": "tsc --noEmit --jsx preserve"
},

View file

@ -1,38 +1,47 @@
jest.mock("axios", () => ({
default: {
get() {
return Promise.reject("NO");
}
interceptors: {
response: { use: jest.fn() },
request: { use: jest.fn() }
},
get() { return Promise.reject("NO"); }
}
}));
import { AuthState } from "../auth/interfaces";
jest.mock("../session", () => {
return {
Session: {
clear: jest.fn(),
getBool: jest.fn(),
}
};
});
import { maybeRefreshToken } from "../refresh_token";
import { API } from "../api/index";
import { Session } from "../session";
API.setBaseUrl("http://blah.whatever.party");
const fakeAuth = (jti = "456"): AuthState => ({
token: {
encoded: "---",
unencoded: {
iat: 123,
jti,
iss: "---",
exp: 456,
mqtt: "---",
os_update_server: "---"
}
}
});
describe("maybeRefreshToken()", () => {
it("gives you the old token when it cant find a new one", (done) => {
maybeRefreshToken(fakeAuth("111"))
.then((nextToken) => {
expect(nextToken.token.unencoded.jti).toEqual("111");
done();
});
it("logs you out when a refresh fails", (done) => {
const t = {
token: {
encoded: "---",
unencoded: {
iat: 123,
jti: "111",
iss: "---",
exp: 456,
mqtt: "---",
os_update_server: "---"
}
}
};
maybeRefreshToken(t).then(() => {
expect(Session.clear).toHaveBeenCalled();
done();
});
});
});

View file

@ -16,6 +16,10 @@ jest.mock("axios", () => ({
default: {
get() {
return Promise.resolve({ data: mockAuth("000") });
},
interceptors: {
response: { use: jest.fn() },
request: { use: jest.fn() }
}
}
}));
@ -24,7 +28,7 @@ import { AuthState } from "../auth/interfaces";
import { maybeRefreshToken } from "../refresh_token";
import { API } from "../api/index";
API.setBaseUrl("http://whateer.party");
API.setBaseUrl("http://whatever.party");
describe("maybeRefreshToken()", () => {
it("gives you back your token when things fail", (done) => {

View file

@ -8,7 +8,8 @@ import {
semverCompare,
SemverResult,
trim,
bitArray
bitArray,
withTimeout
} from "../util";
describe("util", () => {
describe("safeStringFetch", () => {
@ -155,3 +156,21 @@ describe("bitArray", () => {
expect(bitArray(true, true)).toBe(0b11);
});
});
describe("withTimeout()", () => {
it("rejects promises that do not meet a particular deadline", (done) => {
const p = new Promise(res => setTimeout(() => res("Done"), 10));
withTimeout(1, p).then(fail, (y) => {
expect(y).toContain("Timed out");
done();
});
});
it("resolves promises that meet a particular deadline", (done) => {
withTimeout(10, new Promise(res => setTimeout(() => res("Done"), 1)))
.then(y => {
expect(y).toContain("Done");
done();
}, fail);
});
});

View file

@ -21,7 +21,7 @@ import { connectDevice } from "../connectivity/connect_device";
export function didLogin(authState: AuthState, dispatch: Function) {
API.setBaseUrl(authState.token.unencoded.iss);
dispatch(fetchReleases(authState.token.unencoded.os_update_server));
dispatch(loginOk(authState));
dispatch(setToken(authState));
Sync.fetchSyncData(dispatch);
dispatch(connectDevice(authState));
}
@ -30,7 +30,7 @@ export function didLogin(authState: AuthState, dispatch: Function) {
function onLogin(dispatch: Function) {
return (response: HttpData<AuthState>) => {
const { data } = response;
Session.replace(data);
Session.replaceToken(data);
didLogin(data, dispatch);
push("/app/controls");
};
@ -53,12 +53,12 @@ function loginErr() {
/** Very important. Once called, all outbound HTTP requests will
* have a JSON Web Token attached to their "Authorization" header,
* thereby granting access to the API. */
export function loginOk(auth: AuthState): ReduxAction<AuthState> {
export function setToken(auth: AuthState): ReduxAction<AuthState> {
axios.interceptors.response.use(responseFulfilled, responseRejected);
axios.interceptors.request.use(requestFulfilled(auth));
return {
type: Actions.LOGIN_OK,
type: Actions.REPLACE_TOKEN,
payload: auth
};
}
@ -120,7 +120,10 @@ export function logout() {
// In those cases, seeing a logout message may confuse the user.
// To circumvent this, we must check if the user had a token.
// If there was infact a token, we can safely show the message.
if (Session.getAll()) { success(t("You have been logged out.")); }
if (Session.fetchStoredToken()) {
success(t("You have been logged out."));
}
Session.clear();
// Technically this is unreachable code:
return {

View file

@ -3,6 +3,6 @@ import { generateReducer } from "../redux/generate_reducer";
import { Actions } from "../constants";
export let authReducer = generateReducer<AuthState | undefined>(undefined)
.add<AuthState>(Actions.LOGIN_OK, (s, { payload }) => {
.add<AuthState>(Actions.REPLACE_TOKEN, (s, { payload }) => {
return payload;
});

View file

@ -1,23 +1,45 @@
jest.unmock("../../auth/actions");
const actions = require("../../auth/actions");
const didLogin = jest.fn();
const mockState = {
auth: {
token: {
unencoded: { iss: "http://geocities.com" }
}
}
};
jest.mock("axios", () => ({
default: {
interceptors: {
response: { use: jest.fn() },
request: { use: jest.fn() }
},
get() { return Promise.resolve({ data: mockState }); }
}
}));
jest.mock("../../session", () => ({
Session: {
fetchStoredToken: jest.fn(),
getNum: () => undefined,
getBool: () => undefined,
getAll: () => undefined
}
}));
actions.didLogin = didLogin;
import { ready } from "../actions";
const STUB_STATE = { auth: "FOO BAR BAZ" };
jest.mock("../../auth/actions", () => ({
didLogin: jest.fn(),
setToken: jest.fn()
}));
import { ready } from "../actions";
import { setToken } from "../../auth/actions";
describe("Actions", () => {
it("fetches configs and calls didLogin()", () => {
it("calls didLogin()", () => {
jest.resetAllMocks();
const dispatch = jest.fn();
const getState = jest.fn(() => STUB_STATE);
const getState = jest.fn(() => mockState);
const thunk = ready();
thunk(dispatch, getState);
expect(didLogin.mock.calls.length).toBe(1);
expect(setToken).toHaveBeenCalled();
});
});

View file

@ -1,13 +1,31 @@
import { didLogin } from "../auth/actions";
import { didLogin, setToken } from "../auth/actions";
import { Thunk } from "../redux/interfaces";
import { Session } from "../session";
import { maybeRefreshToken } from "../refresh_token";
import { withTimeout } from "../util";
import { AuthState } from "../auth/interfaces";
/** Lets Redux know that the app is ready to bootstrap. */
export const storeToken = (auth: AuthState, dispatch: Function) => () => {
dispatch(setToken(auth));
didLogin(auth, dispatch);
};
/** Amount of time we're willing to wait before concluding that the token is bad
* or the API is down. */
const MAX_TOKEN_WAIT_TIME = 10000;
/** (IMPORTANT) One of the highest level callbacks in the app.
* called once DOM is ready and React is attached to the DOM.
* => Lets Redux know that the app is ready to bootstrap.
* => Checks for token updates since last log in. */
export function ready(): Thunk {
return (dispatch, getState) => {
const state = Session.getAll() || getState().auth;
if (state) {
didLogin(state, dispatch);
const auth = Session.fetchStoredToken() || getState().auth;
if (auth) {
withTimeout(MAX_TOKEN_WAIT_TIME, maybeRefreshToken(auth))
.then(storeToken(auth, dispatch), Session.clear);
} else {
Session.clear();
}
};
}

View file

@ -379,7 +379,7 @@ export enum Actions {
REFRESH_RESOURCE_NO = "REFRESH_RESOURCE_NO",
// Auth
LOGIN_OK = "LOGIN_OK",
REPLACE_TOKEN = "LOGIN_OK",
// Config
CHANGE_API_PORT = "CHANGE_API_PORT",

View file

@ -2,7 +2,6 @@ import { Farmbot } from "farmbot";
import { bail } from "./util";
import { set } from "lodash";
import { AuthState } from "./auth/interfaces";
import { maybeRefreshToken } from "./refresh_token";
let device: Farmbot;
@ -10,15 +9,11 @@ const secure = location.protocol === "https:"; // :(
export const getDevice = (): Farmbot => (device || bail("NO DEVICE SET"));
export function fetchNewDevice(oldToken: AuthState): Promise<Farmbot> {
return maybeRefreshToken(oldToken)
.then(({ token }) => {
device = new Farmbot({ token: token.encoded, secure });
set(window, "current_bot", device);
return device
.connect()
.then(() => {
return device;
}, () => bail("NO CONNECT"));
});
export function fetchNewDevice(auth: AuthState): Promise<Farmbot> {
device = new Farmbot({ token: auth.token.encoded, secure });
set(window, "current_bot", device);
return device
.connect()
.then(() => device, () => bail("NO CONNECT"));
}

View file

@ -1,21 +1,15 @@
/// <reference path="../typings/index.d.ts" />
/**
* THIS IS THE ENTRY POINT FOR THE MAIN PORTION OF THE WEB APP.
*/
import { RootComponent } from "./routes";
import { store } from "./redux/store";
import { ready } from "./config/actions";
*
* Try to keep this file light. */
import { detectLanguage } from "./i18n";
import * as i18next from "i18next";
import { stopIE, attachToRoot, shortRevision } from "./util";
import { stopIE, shortRevision } from "./util";
import { init } from "i18next";
import { attachAppToDom } from "./routes";
stopIE();
console.log(shortRevision());
detectLanguage().then((config) => {
i18next.init(config, (err, t) => {
attachToRoot(RootComponent, { store });
store.dispatch(ready());
});
});
detectLanguage().then(config => init(config, attachAppToDom));

View file

@ -33,7 +33,7 @@ export class FrontPage extends React.Component<{}, Partial<FrontPageState>> {
}
componentDidMount() {
if (Session.getAll()) { window.location.href = "/app/controls"; }
if (Session.fetchStoredToken()) { window.location.href = "/app/controls"; }
logInit();
API.setBaseUrl(API.fetchBrowserLocation());
this.setState({
@ -66,7 +66,7 @@ export class FrontPage extends React.Component<{}, Partial<FrontPageState>> {
API.setBaseUrl(url);
axios.post(API.current.tokensPath, payload)
.then((resp: HttpData<AuthState>) => {
Session.replace(resp.data);
Session.replaceToken(resp.data);
window.location.href = "/app/controls";
}).catch((error: Error) => {
switch (_.get(error, "response.status")) {
@ -308,5 +308,5 @@ export class FrontPage extends React.Component<{}, Partial<FrontPageState>> {
</div>;
}
render() { return Session.getAll() ? <div /> : this.defaultContent(); }
render() { return Session.fetchStoredToken() ? <div /> : this.defaultContent(); }
}

View file

@ -1,6 +1,7 @@
import axios from "axios";
import { Session } from "./session";
import { BooleanSetting } from "./session_keys";
import { InitOptions } from "i18next";
function generateUrl(langCode: string) {
const lang = langCode.slice(0, 2);
@ -15,17 +16,18 @@ function getUserLang(langCode = "en_us") {
.catch((error) => { return "en"; });
}
export function detectLanguage() {
return getUserLang(navigator.language).then(function (lang) {
// NOTE: Some international users prefer using the app in English.
// This preference is stored in `DISABLE_I18N`.
const choice = Session.getBool(BooleanSetting.disableI18n) ? "en" : lang;
const langi = require("../public/app-resources/languages/" + choice + ".js");
return {
nsSeparator: "",
keySeparator: "",
lng: lang,
resources: { [lang]: { translation: langi } }
};
});
function generateI18nConfig(lang: string): InitOptions {
// NOTE: Some users prefer English over i18nized version.
const choice = Session.getBool(BooleanSetting.disableI18n) ? "en" : lang;
const langi = require("../public/app-resources/languages/" + choice + ".js");
return {
nsSeparator: "",
keySeparator: "",
lng: lang,
resources: { [lang]: { translation: langi } }
};
}
export const detectLanguage =
() => getUserLang(navigator.language).then(generateI18nConfig);

View file

@ -6,6 +6,7 @@ import { prettyPrintApiErrors } from "../util";
import { API } from "../api";
import { State, Props } from "./interfaces";
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../ui/index";
import { Session } from "../session";
export class PasswordReset extends React.Component<Props, State> {
constructor() {
@ -41,11 +42,11 @@ export class PasswordReset extends React.Component<Props, State> {
id: token,
password,
password_confirmation: passwordConfirmation,
}).then(() => {
window.location.href = "/";
}).catch((error: string) => {
log(prettyPrintApiErrors(error as {}));
});
})
.then(Session.clear)
.catch((error: string) => {
log(prettyPrintApiErrors(error as {}));
});
}
render() {

View file

@ -2,9 +2,21 @@ import axios from "axios";
import { API } from "./api/index";
import { AuthState } from "./auth/interfaces";
import { HttpData } from "./util";
import { setToken } from "./auth/actions";
import { Session } from "./session";
export let maybeRefreshToken = (old: AuthState): Promise<AuthState> => {
return axios
.get(API.current.tokensPath)
.then((x: HttpData<AuthState>) => x.data, () => (old));
type Resp = HttpData<AuthState>;
/** What to do when the Token refresh request completes. */
const ok = (x: Resp) => {
setToken(x.data); // Start using new token in HTTP requests.
return x.data;
};
/** Grab a new token from the API (won't extend token's exp. date).
* Redirect to home page on failure. */
export let maybeRefreshToken = (old: AuthState): Promise<AuthState> => {
API.setBaseUrl(old.token.unencoded.iss);
setToken(old); // Precaution: The Axios interceptors might not be set yet.
return axios.get(API.current.tokensPath).then(ok, Session.clear);
};

View file

@ -9,7 +9,8 @@ import { history } from "./history";
import { Store } from "./redux/interfaces";
import { ready } from "./config/actions";
import { Session } from "./session";
import { isMobile } from "./util";
import { isMobile, attachToRoot } from "./util";
import { Callback } from "i18next";
interface RootComponentProps {
store: Store;
@ -66,11 +67,17 @@ const controlsRoute = {
).catch(errorLoading(cb));
}
};
export const attachAppToDom: Callback = (err, t) => {
attachToRoot(RootComponent, { store: _store });
_store.dispatch(ready());
};
export class RootComponent extends React.Component<RootComponentProps, {}> {
requireAuth(_discard: RouterState, replace: RedirectFunction) {
const { store } = this.props;
if (Session.getAll()) { // has a previous session in cache
if (Session.fetchStoredToken()) { // has a previous session in cache
if (store.getState().auth) { // Has session, logged in.
return;
} else { // Has session but not logged in (returning visitor).
@ -282,10 +289,10 @@ export class RootComponent extends React.Component<RootComponentProps, {}> {
render() {
// ==== TEMPORARY HACK. TODO: Add a before hook, if such a thing exists in
// React Router. Or switch routing libs.
const notLoggedIn = !Session.getAll();
const notLoggedIn = !Session.fetchStoredToken();
const restrictedArea = window.location.pathname.includes("/app");
if (notLoggedIn && restrictedArea) {
window.location.href = "/";
Session.clear();
}
// ==== END HACK ====
return <Provider store={_store}>

View file

@ -17,12 +17,12 @@ export namespace Session {
const KEY = "session";
/** Replace the contents of session storage. */
export function replace(nextState: AuthState) {
export function replaceToken(nextState: AuthState) {
localStorage[KEY] = JSON.stringify(nextState);
}
/** Fetch the previous session. */
export function getAll(): AuthState | undefined {
export function fetchStoredToken(): AuthState | undefined {
try {
const v: AuthState = JSON.parse(localStorage[KEY]);
if (box(v).kind === "object") {
@ -36,10 +36,11 @@ export namespace Session {
}
/** Clear localstorage and sessionstorage. */
export function clear() {
export function clear(): never {
localStorage.clear();
sessionStorage.clear();
window.location.href = window.location.origin;
// window.location.href = window.location.origin;
throw new Error("session cleared");
}
/** Fetch a *boolean* value from localstorage. Returns `undefined` when
@ -74,9 +75,7 @@ export namespace Session {
const isBooleanSetting =
// tslint:disable-next-line:no-any
(x: any): x is BooleanSetting => {
return !!BooleanSetting[x];
};
(x: any): x is BooleanSetting => !!BooleanSetting[x];
export function safeBooleanSettting(name: string): BooleanSetting {
if (isBooleanSetting(name)) {

View file

@ -52,7 +52,7 @@ export class Wow extends React.Component<Props, Partial<State>> {
axios
.post(API.current.tokensPath, payload)
.then((resp: HttpData<AuthState>) => {
Session.replace(resp.data);
Session.replaceToken(resp.data);
window.location.href = "/app/controls";
})
.catch(error => {

View file

@ -235,14 +235,14 @@ export function smoothScrollToBottom() {
let timer = 0;
if (stopY > startY) {
for (let i = startY; i < stopY; i += step) {
setTimeout("window.scrollTo(0, " + leapY + ")", timer * speed);
setTimeout(() => window.scrollTo(0, leapY), timer * speed);
leapY += step;
if (leapY > stopY) { leapY = stopY; }
timer++;
} return;
}
for (let i = startY; i > stopY; i -= step) {
setTimeout("window.scrollTo(0, " + leapY + ")", timer * speed);
setTimeout(() => window.scrollTo(0, leapY), timer * speed);
leapY -= step; if (leapY < stopY) { leapY = stopY; }
timer++;
}
@ -447,3 +447,19 @@ export function bitArray(...values: boolean[]) {
export function bail(message: string): never {
throw new Error(message);
}
// Thanks,
// https://italonascimento.github.io
// /applying-a-timeout-to-your-promises/#implementing-the-timeout
export function withTimeout<T>(ms: number, promise: Promise<T>) {
// Create a promise that rejects in <ms> milliseconds
const timeout = new Promise((resolve, reject) => {
const id = setTimeout(() => {
clearTimeout(id);
reject(`Timed out in ${ms} ms.`);
}, ms);
});
// Returns a race between our timeout and the passed in promise
return Promise.race([promise, timeout]);
}

View file

@ -13,7 +13,7 @@ export async function verify() {
const url = API.fetchBrowserLocation();
try {
let r: HttpData<AuthState> = await axios.put(url + "/api/users/verify/" + token);
Session.replace(r.data);
Session.replaceToken(r.data);
window.location.href = window.location.origin + "/app/controls";
} catch (e) {
document.write(`

View file

@ -117,13 +117,13 @@
dependencies:
axios "^0.16.1"
"@types/mqtt@^2.5.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@types/mqtt/-/mqtt-2.5.0.tgz#bc54c2d53f509282168da4a9af865de95bee5101"
"@types/mqtt@^0.0.34":
version "0.0.34"
resolved "https://registry.yarnpkg.com/@types/mqtt/-/mqtt-0.0.34.tgz#7865790000cc8a312242ead9a0e209ba2aa686d4"
dependencies:
mqtt "*"
"@types/node" "*"
"@types/node@^8.0.34":
"@types/node@*", "@types/node@^8.0.34":
version "8.0.34"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.34.tgz#55f801fa2ddb2a40dd6dfc15ecfe1dde9c129fe9"
@ -399,10 +399,6 @@ async-foreach@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
async-limiter@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
async@^1.4.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@ -645,12 +641,6 @@ bl@^0.9.1:
dependencies:
readable-stream "~1.0.26"
bl@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e"
dependencies:
readable-stream "^2.0.5"
blamer@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/blamer/-/blamer-0.1.13.tgz#61f215f2361bd054e6258c0c5e0086f04074e670"
@ -1114,7 +1104,7 @@ concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
concat-stream@^1.4.7, concat-stream@^1.6.0:
concat-stream@^1.4.7:
version "1.6.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
dependencies:
@ -2435,7 +2425,7 @@ hawk@3.1.3, hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
help-me@^1.0.0, help-me@^1.0.1:
help-me@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/help-me/-/help-me-1.1.0.tgz#8f2d508d0600b4a456da2f086556e7e5c056a3c6"
dependencies:
@ -3827,33 +3817,6 @@ mqtt-packet@^3.0.0, mqtt-packet@^3.4.7:
bl "^0.9.1"
inherits "^2.0.1"
mqtt-packet@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/mqtt-packet/-/mqtt-packet-5.4.0.tgz#387104c06aa68fbb9f8159d0c722dd5c3e45df22"
dependencies:
bl "^1.2.1"
inherits "^2.0.3"
process-nextick-args "^1.0.7"
safe-buffer "^5.1.0"
mqtt@*:
version "2.13.0"
resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-2.13.0.tgz#cbf3fae8f0c48328472b0d5f8e049cc800957d7e"
dependencies:
commist "^1.0.0"
concat-stream "^1.6.0"
end-of-stream "^1.1.0"
help-me "^1.0.1"
inherits "^2.0.3"
minimist "^1.2.0"
mqtt-packet "^5.4.0"
pump "^1.0.2"
readable-stream "^2.3.3"
reinterval "^1.1.0"
split2 "^2.1.1"
websocket-stream "^5.0.1"
xtend "^4.0.1"
mqtt@^1.7.4:
version "1.14.1"
resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-1.14.1.tgz#7e376987153d01793e946d26d46122ebf0c03554"
@ -4711,7 +4674,7 @@ private@^0.1.6:
version "0.1.7"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
process-nextick-args@^1.0.7, process-nextick-args@~1.0.6:
process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
@ -4757,7 +4720,7 @@ public-encrypt@^4.0.0:
parse-asn1 "^5.0.0"
randombytes "^2.0.1"
pump@^1.0.0, pump@^1.0.1, pump@^1.0.2:
pump@^1.0.0, pump@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.2.tgz#3b3ee6512f94f0e575538c17995f9f16990a5d51"
dependencies:
@ -4970,7 +4933,7 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
"readable-stream@> 1.0.0 < 3.0.0", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.0, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9, readable-stream@^2.3.3:
"readable-stream@> 1.0.0 < 3.0.0", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
dependencies:
@ -5093,7 +5056,7 @@ regjsparser@^0.1.4:
dependencies:
jsesc "~0.5.0"
reinterval@^1.0.1, reinterval@^1.1.0:
reinterval@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7"
@ -5209,7 +5172,7 @@ safe-buffer@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@ -5484,12 +5447,6 @@ split2@^2.0.1:
dependencies:
through2 "^2.0.2"
split2@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
dependencies:
through2 "^2.0.2"
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@ -5948,10 +5905,6 @@ ultron@1.0.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
ultron@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864"
unc-path-regex@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
@ -6232,17 +6185,6 @@ websocket-stream@^3.0.1:
ws "^1.0.1"
xtend "^4.0.0"
websocket-stream@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/websocket-stream/-/websocket-stream-5.0.1.tgz#51cb992988c2eeb4525ccd90eafbac52a5ac6700"
dependencies:
duplexify "^3.2.0"
inherits "^2.0.1"
readable-stream "^2.2.0"
safe-buffer "^5.0.1"
ws "^3.0.0"
xtend "^4.0.0"
weinre@^2.0.0-pre-I0Z7U9OV:
version "2.0.0-pre-I0Z7U9OV"
resolved "https://registry.yarnpkg.com/weinre/-/weinre-2.0.0-pre-I0Z7U9OV.tgz#fef8aa223921f7b40bbbbd4c3ed4302f6fd0a813"
@ -6352,14 +6294,6 @@ ws@^1.0.1:
options ">=0.0.5"
ultron "1.0.x"
ws@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-3.2.0.tgz#d5d3d6b11aff71e73f808f40cc69d52bb6d4a185"
dependencies:
async-limiter "~1.0.0"
safe-buffer "~5.1.0"
ultron "~1.1.0"
xml-name-validator@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
@ -6377,7 +6311,7 @@ xmlbuilder@^4.1.0:
dependencies:
lodash "^4.0.0"
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"