use comma-api

main
Andy Haden 2019-06-14 14:39:49 -07:00
parent 4bb6453247
commit 839607863b
14 changed files with 171 additions and 417 deletions

View File

@ -5,7 +5,7 @@
* sudo apt-get install -y libusb-dev libudev-dev ruby-sass
* yarn install
## Developemnt
## Development
* yarn run sass
* yarn start
@ -15,7 +15,7 @@
* npm version patch
* git push # push version patch
* yarn build
* scripts/deploy.sh
* CONTAINER=cabana scripts/deploy.sh
# Create React App documentation

View File

@ -4,8 +4,10 @@
"private": true,
"homepage": "https://community.comma.ai/cabana",
"dependencies": {
"@commaai/comma-api": "^1.0.7",
"@commaai/hls.js": "^0.12.2",
"@commaai/log_reader": "^0.3.1",
"@commaai/my-comma-auth": "^1.0.3",
"@commaai/pandajs": "^0.3.4",
"ap": "^0.2.0",
"aphrodite": "^1.2.1",
@ -71,7 +73,7 @@
"xtend": "^4.0.1"
},
"scripts": {
"start": "react-app-rewired start",
"start": "PORT=3001 react-app-rewired start",
"build": "react-app-rewired build",
"build:staging": "env-cmd .env.staging react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",

View File

@ -1,14 +1,17 @@
#!/bin/bash
set -e
set -x
CONTAINER=${CONTAINER:-cabana-staging}
pushd build/
find . -not -name "*.map" -type f | while read f; do
az storage blob upload --account-name chffrdist --file "$f" --container-name cabana --name "$f"
az storage blob upload --account-name chffrdist --file "$f" --container-name $CONTAINER --name "$f"
done
popd
pushd public
find img -type f | while read f; do
az storage blob upload --account-name chffrdist --file "$f" --container-name cabana --name "$f"
az storage blob upload --account-name chffrdist --file "$f" --container-name $CONTAINER --name "$f"
done
popd

View File

@ -4,15 +4,14 @@ import PropTypes from "prop-types";
import cx from "classnames";
import { createWriteStream } from "streamsaver";
import Panda from "@commaai/pandajs";
import CommaAuth from "@commaai/my-comma-auth";
import { raw as RawDataApi, drives as DrivesApi } from "@commaai/comma-api";
import { USE_UNLOGGER, PART_SEGMENT_LENGTH, STREAMING_WINDOW } from "./config";
import * as GithubAuth from "./api/github-auth";
import * as auth from "./api/comma-auth";
import DBC from "./models/can/dbc";
import Meta from "./components/Meta";
import Explorer from "./components/Explorer";
import * as Routes from "./api/routes";
import OnboardingModal from "./components/Modals/OnboardingModal";
import SaveDbcModal from "./components/SaveDbcModal";
import LoadDbcModal from "./components/LoadDbcModal";
@ -53,7 +52,6 @@ export default class CanExplorer extends Component {
messages: {},
selectedMessages: [],
route: null,
routes: [],
canFrameOffset: -1,
firstCanTime: null,
lastBusTime: null,
@ -123,10 +121,13 @@ export default class CanExplorer extends Component {
componentWillMount() {
const { dongleId, name } = this.props;
if (this.props.max && this.props.url) {
if (CommaAuth.isAuthenticated() && !name) {
// TODO link to explorer
this.showOnboarding();
} else if (this.props.max && this.props.url) {
// probably the demo!
const { max, url } = this.props;
const { startTime } = Routes.parseRouteName(name);
const startTime = Moment(name, "YYYY-MM-DD--H-m-s");
const route = {
fullname: dongleId + "|" + name,
@ -141,40 +142,35 @@ export default class CanExplorer extends Component {
},
this.initCanData
);
} else if (auth.isAuthenticated() && !name) {
Routes.fetchRoutes()
.then(routes => {
const _routes = [];
Object.keys(routes).forEach(route => {
_routes.push(routes[route]);
});
this.setState({ routes: _routes });
if (!_routes[name]) {
this.showOnboarding();
}
})
.catch(err => {
this.showOnboarding();
});
} else if (dongleId && name) {
Routes.fetchRoutes(dongleId)
.then(routes => {
if (routes && routes[name]) {
// this makes fullname = dongleId + '|' + name
const route = routes[name];
const newState = {
route,
currentParts: [
0,
Math.min(route.proclog, PART_SEGMENT_LENGTH - 1)
]
};
this.setState(newState, this.initCanData);
} else {
this.showOnboarding();
}
const routeName = dongleId + "|" + name;
let urlPromise;
if (this.props.url) {
urlPromise = Promise.resolve(this.props.url);
} else {
urlPromise = DrivesApi.getRouteInfo(routeName).then(function(route) {
return route.url;
});
}
Promise.all([urlPromise, RawDataApi.getLogUrls(routeName)])
.then(initData => {
let [url, logUrls] = initData;
const newState = {
route: {
fullname: routeName,
proclog: logUrls.length - 1,
start_time: Moment(name, "YYYY-MM-DD--H-m-s"),
url
},
currentParts: [
0,
Math.min(logUrls.length - 1, PART_SEGMENT_LENGTH - 1)
]
};
this.setState(newState, this.initCanData);
})
.catch(err => {
console.error(err);
this.showOnboarding();
});
} else {

View File

@ -1,57 +0,0 @@
// API gateway for `api.commadotai.com/v1` urls
import { getCommaAccessToken } from "./comma-auth";
const URL_ROOT = "https://api.commadotai.com/v1/";
const ConfigRequest = require("config-request/instance");
const request = ConfigRequest();
var initPromise = init();
async function init() {
var token = await getCommaAccessToken();
request.configure({
baseUrl: URL_ROOT,
token: "JWT " + token,
jwt: false
});
}
export async function get(endpoint, data) {
await initPromise;
return new Promise((resolve, reject) => {
request.get(
endpoint,
{
query: data,
json: true
},
errorHandler(resolve, reject)
);
});
}
export async function post(endpoint, data) {
await initPromise;
return new Promise((resolve, reject) => {
request.post(
endpoint,
{
body: data,
json: true
},
errorHandler(resolve, reject)
);
});
}
function errorHandler(resolve, reject) {
return handle;
function handle(err, data) {
if (err) {
return reject(err);
}
resolve(data);
}
}

View File

@ -1,52 +0,0 @@
import Cookies from "js-cookie";
import storage from "localforage";
import {
COMMA_ACCESS_TOKEN_COOKIE,
COMMA_OAUTH_REDIRECT_COOKIE
} from "../config";
let isAuthed = false;
let useForage = true;
export async function getCommaAccessToken() {
let token = getTokenInternal();
if (!token) {
try {
token = await storage.getItem("authorization");
} catch (e) {
useForage = false;
}
}
if (token) {
isAuthed = true;
if (useForage) {
await storage.setItem("authorization", token);
}
}
return token;
}
// seed cache
getCommaAccessToken();
function getTokenInternal() {
if (typeof localStorage !== "undefined") {
if (localStorage.authorization) {
return localStorage.authorization;
}
}
return Cookies.get(COMMA_ACCESS_TOKEN_COOKIE);
}
export function isAuthenticated() {
return isAuthed;
}
export function authUrl() {
Cookies.set(COMMA_OAUTH_REDIRECT_COOKIE, window.location.href);
return "https://community.comma.ai/forum/ucp.php?mode=login&login=external&oauth_service=google";
}

View File

@ -1,13 +1,27 @@
import * as CommaAPI from "./comma-api";
import { raw as RawDataApi, request as Request } from "@commaai/comma-api";
import CommaAuth from "@commaai/my-comma-auth";
import request from "simple-get";
const urlStore = {};
var initPromise;
function ensureInit() {
if (!initPromise) {
initPromise = CommaAuth.init().then(function(token) {
Request.configure(token);
return Promise.resolve();
});
}
return initPromise;
}
export async function getLogURLList(routeName) {
if (urlStore[routeName]) {
return urlStore[routeName];
}
var data = await CommaAPI.get("route/" + routeName + "/log_urls");
await ensureInit();
var data = await RawDataApi.getLogUrls(routeName);
urlStore[routeName] = data;

View File

@ -1,45 +0,0 @@
import Moment from "moment";
import * as CommaAuth from "./comma-auth";
const ROUTES_ENDPOINT = "https://api.commadotai.com/v1/{dongleId}/routes/";
function momentizeTimes(routes) {
for (let routeName in routes) {
routes[routeName].start_time = Moment(routes[routeName].start_time);
routes[routeName].end_time = Moment(routes[routeName].end_time);
}
return routes;
}
export async function fetchRoutes(dongleId) {
// will throw errors from fetch() on HTTP failure
if (dongleId === undefined) {
dongleId = "me";
}
const accessToken = await CommaAuth.getCommaAccessToken();
if (accessToken) {
const endpoint = ROUTES_ENDPOINT.replace("{dongleId}", dongleId);
const headers = new Headers();
headers.append("Authorization", `JWT ${accessToken}`);
const request = new Request(endpoint, { headers });
const resp = await fetch(request);
const routes = await resp.json();
if ("routes" in routes) {
return momentizeTimes(routes.routes);
}
}
return {};
}
export function cameraPath(routeUrl, frame) {
return `${routeUrl}/sec${frame}.jpg`;
}
export function parseRouteName(name) {
const startTime = Moment(name, "YYYY-MM-DD--H-m-s");
return { startTime };
}

View File

@ -1,26 +0,0 @@
const STREAM_VERSION = 2;
function videoUrl(dongleId, hashedRouteName) {
return `${
process.env.REACT_APP_VIDEO_CDN
}/hls/${dongleId}/${hashedRouteName}/index.m3u8?v=${STREAM_VERSION}`;
}
function videoUrlForRouteUrl(routeUrlString) {
const url = new URL(routeUrlString);
const pathParts = url.pathname.split("/");
const [dongleIdPrefixed, hashedRouteName] = pathParts.slice(
pathParts.length - 2
);
let dongleId = dongleIdPrefixed;
if (dongleIdPrefixed.indexOf("comma-") === 0) {
const [, dongleIdNoPrefix] = dongleIdPrefixed.split("comma-");
dongleId = dongleIdNoPrefix;
}
return videoUrl(dongleId, hashedRouteName);
}
export default { videoUrl, videoUrlForRouteUrl };

View File

@ -3,9 +3,9 @@ import PropTypes from "prop-types";
import Moment from "moment";
import _ from "lodash";
import cx from "classnames";
import CommaAuth from "@commaai/my-comma-auth";
import * as auth from "../../api/comma-auth";
import { EXPLORER_URL } from "../../config";
import Modal from "../Modals/baseModal";
export default class OnboardingModal extends Component {
@ -25,18 +25,12 @@ export default class OnboardingModal extends Component {
this.state = {
webUsbEnabled: !!navigator.usb,
viewingUsbInstructions: false,
pandaConnected: false,
chffrDrivesSearch: "",
chffrDrivesSortBy: "start_time",
chffrDrivesOrderDesc: true
pandaConnected: false
};
this.attemptPandaConnection = this.attemptPandaConnection.bind(this);
this.toggleUsbInstructions = this.toggleUsbInstructions.bind(this);
this.handleSortDrives = this.handleSortDrives.bind(this);
this.handleSearchDrives = this.handleSearchDrives.bind(this);
this.navigateToAuth = this.navigateToAuth.bind(this);
this.openChffrDrive = this.openChffrDrive.bind(this);
this.navigateToExplorer = this.navigateToExplorer.bind(this);
}
attemptPandaConnection() {
@ -52,53 +46,14 @@ export default class OnboardingModal extends Component {
});
}
navigateToAuth() {
const authUrl = auth.authUrl();
window.location.href = authUrl;
navigateToExplorer() {
window.location.href = EXPLORER_URL;
}
filterRoutesWithCan(drive) {
return drive.can === true;
}
handleSearchDrives(drive) {
const { chffrDrivesSearch } = this.state;
const searchKeywords = chffrDrivesSearch
.split(" ")
.filter(s => s.length > 0)
.map(s => s.toLowerCase());
return (
searchKeywords.length === 0 ||
searchKeywords.some(
kw =>
drive.end_geocode.toLowerCase().indexOf(kw) !== -1 ||
drive.start_geocode.toLowerCase().indexOf(kw) !== -1 ||
Moment(drive.start_time)
.format("dddd MMMM Do YYYY")
.toLowerCase()
.indexOf(kw) !== -1 ||
Moment(drive.end_time)
.format("dddd MMMM Do YYYY")
.toLowerCase()
.indexOf(kw) !== -1
)
);
}
handleSortDrives(key) {
if (this.state.chffrDrivesSortBy === key) {
this.setState({ chffrDrivesOrderDesc: !this.state.chffrDrivesOrderDesc });
} else {
this.setState({ chffrDrivesOrderDesc: true });
this.setState({ chffrDrivesSortBy: key });
}
}
openChffrDrive(route) {
window.location.search = `?route=${route.fullname}`;
}
renderPandaEligibility() {
const { webUsbEnabled, pandaConnected } = this.state;
const { attemptingPandaConnection } = this.props;
@ -123,140 +78,27 @@ export default class OnboardingModal extends Component {
}
}
renderChffrOption() {
const { routes } = this.props;
if (routes.length > 0) {
return (
<div className="cabana-onboarding-mode-chffr">
<div className="cabana-onboarding-mode-chffr-search">
<div className="form-field--small">
<input
type="text"
id="chffr_drives_search"
placeholder="Search chffr drives"
value={this.state.chffrDrivesSearch}
onChange={e =>
this.setState({ chffrDrivesSearch: e.target.value })
}
/>
<div className="cabana-onboarding-mode-chffr-search-helper">
<p>(Try: "Drives in San Francisco" or "Drives in June 2017")</p>
</div>
</div>
</div>
<div
className={cx("cabana-onboarding-mode-chffr-header", {
"is-ordered-desc": this.state.chffrDrivesOrderDesc,
"is-ordered-asc": !this.state.chffrDrivesOrderDesc
})}
>
<div
className={cx("cabana-onboarding-mode-chffr-drive-date", {
"is-sorted": this.state.chffrDrivesSortBy === "start_time"
})}
onClick={() => this.handleSortDrives("start_time")}
>
<span>Date</span>
</div>
<div
className={cx("cabana-onboarding-mode-chffr-drive-places", {
"is-sorted": this.state.chffrDrivesSortBy === "end_geocode"
})}
onClick={() => this.handleSortDrives("end_geocode")}
>
<span>Places</span>
</div>
<div className={cx("cabana-onboarding-mode-chffr-drive-time")}>
<span>Time</span>
</div>
<div
className={cx("cabana-onboarding-mode-chffr-drive-distance", {
"is-sorted": this.state.chffrDrivesSortBy === "len"
})}
onClick={() => this.handleSortDrives("len")}
>
<span>Distance</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-action" />
</div>
<ul className="cabana-onboarding-mode-chffr-drives">
{_.orderBy(
routes,
[this.state.chffrDrivesSortBy],
[this.state.chffrDrivesOrderDesc ? "desc" : "asc"]
)
.filter(this.filterRoutesWithCan)
.filter(this.handleSearchDrives)
.map(route => {
const routeDuration = Moment.duration(
route.end_time.diff(route.start_time)
);
const routeStartClock = Moment(route.start_time).format("LT");
const routeEndClock = Moment(route.end_time).format("LT");
return (
<li
key={route.fullname}
className="cabana-onboarding-mode-chffr-drive"
>
<div className="cabana-onboarding-mode-chffr-drive-date">
<strong>
{Moment(route.start_time._i).format("MMM Do")}
</strong>
<span>{Moment(route.start_time._i).format("dddd")}</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-places">
<strong>{route.end_geocode}</strong>
<span>From {route.start_geocode}</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-time">
<strong>
{routeDuration.hours > 0
? `${routeDuration._data.hours} hr `
: null}
{`${routeDuration._data.minutes} min ${
routeDuration._data.seconds
} sec`}
</strong>
<span>{`${routeStartClock} - ${routeEndClock}`}</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-distance">
<strong>{route.len.toFixed(2)} mi</strong>
<span>{(route.len * 1.6).toFixed(2)} km</span>
</div>
<div className="cabana-onboarding-mode-chffr-drive-action">
<button
className="button--primary"
onClick={() => this.openChffrDrive(route)}
>
<span>View Drive</span>
</button>
</div>
</li>
);
})}
</ul>
</div>
);
} else {
return (
<button
onClick={this.navigateToAuth}
className="button--primary button--kiosk"
>
<i className="fa fa-video-camera" />
<strong>Log in to View Recorded Drives</strong>
<sup>
Analyze your car driving data from <em>chffr</em>
</sup>
</button>
);
}
renderLogin() {
return (
<button
onClick={this.navigateToExplorer}
className="button--primary button--kiosk"
>
<i className="fa fa-video-camera" />
<strong>
{CommaAuth.isAuthenticated()
? "Find a drive in Explorer"
: "Log in with Explorer"}
</strong>
<sup>Click "View CAN Data" while replaying a drive</sup>
</button>
);
}
renderOnboardingOptions() {
return (
<div className="cabana-onboarding-modes">
<div className="cabana-onboarding-mode">{this.renderChffrOption()}</div>
<div className="cabana-onboarding-mode">{this.renderLogin()}</div>
<div className="cabana-onboarding-mode">
<button
className={cx("button--secondary button--kiosk", {
@ -371,8 +213,8 @@ export default class OnboardingModal extends Component {
render() {
return (
<Modal
title="Welcome to cabana"
subtitle="Get started by viewing your chffr drives or enabling live mode"
title="Welcome to Cabana"
subtitle="Get started by selecting a drive from Explorer or enabling live mode"
footer={this.renderModalFooter()}
disableClose={true}
variations={["wide", "dark"]}

View File

@ -1,10 +1,9 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { StyleSheet, css } from "aphrodite/no-important";
import { derived as RouteApi, video as VideoApi } from "@commaai/comma-api";
import HLS from "./HLS";
import { cameraPath } from "../api/routes";
import Video from "../api/video";
import RouteSeeker from "./RouteSeeker/RouteSeeker";
const Styles = StyleSheet.create({
@ -93,7 +92,7 @@ export default class RouteVideoSync extends Component {
nearestFrameUrl() {
const { url } = this.props;
const sec = Math.round(this.props.userSeekTime);
return cameraPath(url, sec);
return RouteApi(url).getJpegUrl(sec);
}
loadingOverlay() {
@ -170,7 +169,7 @@ export default class RouteVideoSync extends Component {
) : null}
<HLS
className={css(Styles.hls)}
source={Video.videoUrlForRouteUrl(this.props.url)}
source={VideoApi(this.props.url).getRearCameraStreamIndexUrl()}
startTime={this.props.userSeekTime}
playbackSpeed={this.props.playSpeed}
onVideoElementAvailable={this.onVideoElementAvailable}

View File

@ -28,5 +28,8 @@ export const CAN_GRAPH_MAX_POINTS = 10000;
export const STREAMING_WINDOW = 60;
export const COMMA_ACCESS_TOKEN_COOKIE = "comma_access_token";
export const COMMA_OAUTH_REDIRECT_COOKIE = "wiki_login_redirect";
const ENV_EXPLORER_URL = {
debug: "http://127.0.0.1:3000/",
prod: "https://my.comma.ai/"
};
export const EXPLORER_URL = ENV_EXPLORER_URL[ENV];

View File

@ -1,6 +1,8 @@
import Sentry from "./logging/Sentry";
import React from "react";
import ReactDOM from "react-dom";
import CommaAuth from "@commaai/my-comma-auth";
import { request as Request } from "@commaai/comma-api";
import CanExplorer from "./CanExplorer";
import AcuraDbc from "./acura-dbc";
import { getUrlParameter, modifyQueryParameters } from "./utils/url";
@ -28,11 +30,13 @@ if (routeFullName) {
let max = getUrlParameter("max"),
url = getUrlParameter("url");
if (max && url) {
if (max) {
props.max = max;
props.url = url;
props.isShare = true;
}
if (url) {
props.url = url;
}
props.isShare = max && url;
} else if (getUrlParameter("demo")) {
props.max = 12;
props.url =
@ -78,8 +82,16 @@ if (authTokenQueryParam !== null) {
props.githubAuthToken = fetchPersistedGithubAuthToken();
}
if (routeFullName || isDemo) {
async function init() {
const token = await CommaAuth.init();
if (token) {
Request.configure(token);
}
ReactDOM.render(<CanExplorer {...props} />, document.getElementById("root"));
}
if (routeFullName || isDemo) {
init();
} else {
const img = document.createElement("img");
img.src = process.env.PUBLIC_URL + "/img/cabana.jpg";

View File

@ -17,6 +17,16 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
"@commaai/comma-api@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@commaai/comma-api/-/comma-api-1.0.9.tgz#f5627c47392107c600cbb4f419ad65b89a6ccf35"
integrity sha512-NJPL2dRK9c+ipfOlS3mr9+DFZSUyYAxcbHZysagH5ChPKHXtSbggQKS/KValLA7bxdXVBhNalh21ufmqoQIejg==
dependencies:
babel-runtime "^6.26.0"
config-request "^0.5.1"
joi-browser "^13.4.0"
querystringify "^2.1.1"
"@commaai/hls.js@^0.12.2":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@commaai/hls.js/-/hls.js-0.12.2.tgz#694433f0de5e454a1b116daf83f9f6d4b6f285ab"
@ -39,6 +49,18 @@
geval "^2.2.0"
stream-selector "^0.4.0"
"@commaai/my-comma-auth@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@commaai/my-comma-auth/-/my-comma-auth-1.0.3.tgz#030e95aa2ef8775824e453f67519544ef61f6bf0"
integrity sha512-S4cSV0l5WV4vUQ3N+3wRS2oWX7Bk9PnoXU3sV9IDTGIotsjxeJOw9SRsknqYEGnOc3UwPg73OIyQAyH3qmuWvA==
dependencies:
babel-runtime "^6.26.0"
comma-api "https://github.com/commaai/comma-api.git#full-spec"
config-request "^0.5.1"
global "^4.4.0"
localforage "^1.7.3"
querystringify "^2.1.1"
"@commaai/pandajs@^0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@commaai/pandajs/-/pandajs-0.3.4.tgz#b8ad7f1e1cee2cce15f14f38cbf6f1ba8162814b"
@ -1252,6 +1274,7 @@ babel-register@^6.26.0:
babel-runtime@6.26.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
@ -1976,6 +1999,15 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
"comma-api@https://github.com/commaai/comma-api.git#full-spec":
version "1.0.7"
resolved "https://github.com/commaai/comma-api.git#6dc346876c16aa4ac48884c1261b2bb29720cbb0"
dependencies:
babel-runtime "^6.26.0"
config-request "^0.5.1"
joi-browser "^13.4.0"
querystringify "^2.1.1"
commander@2, commander@2.15.x, commander@^2.11.0, commander@^2.15.1, commander@^2.9.0, commander@~2.15.0:
version "2.15.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
@ -2115,7 +2147,12 @@ core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0:
core-js@^2.4.0:
version "2.6.9"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
core-js@^2.4.1, core-js@^2.5.0:
version "2.5.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b"
@ -3919,6 +3956,14 @@ global@^4.3.2, global@~4.3.0:
min-document "^2.19.0"
process "~0.5.1"
global@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
dependencies:
min-document "^2.19.0"
process "^0.11.10"
globals@^9.17.0, globals@^9.18.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@ -5161,6 +5206,11 @@ jest@20.0.4:
dependencies:
jest-cli "^20.0.4"
joi-browser@^13.4.0:
version "13.4.0"
resolved "https://registry.yarnpkg.com/joi-browser/-/joi-browser-13.4.0.tgz#b72ba61b610e3f58e51b563a14e0f5225cfb6896"
integrity sha512-TfzJd2JaJ/lg/gU+q5j9rLAjnfUNF9DUmXTP9w+GfmG79LjFOXFeM7hIFuXCBcZCivUDFwd9l1btTV9rhHumtQ==
js-base64@^2.1.8, js-base64@^2.1.9:
version "2.4.3"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
@ -5500,6 +5550,13 @@ localforage@^1.7.1:
dependencies:
lie "3.1.1"
localforage@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.3.tgz#0082b3ca9734679e1bd534995bdd3b24cf10f204"
integrity sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==
dependencies:
lie "3.1.1"
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@ -7107,6 +7164,11 @@ querystringify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755"
querystringify@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
raf@3.4.0, raf@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
@ -7423,6 +7485,7 @@ regenerate@^1.2.1:
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-transform@^0.10.0:
version "0.10.1"