diff --git a/.env.sample b/.env.sample index 50c099999..8e56d94aa 100644 --- a/.env.sample +++ b/.env.sample @@ -12,6 +12,7 @@ REDIS_URL=redis://redis:6379 URL=http://localhost:3000 DEPLOYMENT=self ENABLE_UPDATES=true +SUBDOMAINS_ENABLED=false DEBUG=sql,cache,presenters,events # Third party signin credentials (at least one is required) diff --git a/app/components/Button/Button.js b/app/components/Button/Button.js index 1e455e654..a37921bf1 100644 --- a/app/components/Button/Button.js +++ b/app/components/Button/Button.js @@ -32,7 +32,7 @@ const RealButton = styled.button` } &:disabled { - opacity: 0.8; + opacity: 0.6; cursor: default; } diff --git a/app/components/Sidebar/components/HeaderBlock.js b/app/components/Sidebar/components/HeaderBlock.js index 3b97dc654..1dea87eba 100644 --- a/app/components/Sidebar/components/HeaderBlock.js +++ b/app/components/Sidebar/components/HeaderBlock.js @@ -3,7 +3,7 @@ import * as React from 'react'; import styled, { withTheme } from 'styled-components'; import { ExpandedIcon } from 'outline-icons'; import Flex from 'shared/components/Flex'; -import TeamLogo from './TeamLogo'; +import TeamLogo from 'shared/components/TeamLogo'; type Props = { teamName: string, diff --git a/app/menus/RevisionMenu.js b/app/menus/RevisionMenu.js index a1a3a1043..5e3cabac3 100644 --- a/app/menus/RevisionMenu.js +++ b/app/menus/RevisionMenu.js @@ -36,7 +36,7 @@ class RevisionMenu extends React.Component { render() { const { label, className, onOpen, onClose } = this.props; - const url = `${process.env.URL}${documentHistoryUrl( + const url = `${window.location.origin}${documentHistoryUrl( this.props.document, this.props.revision.id )}`; diff --git a/app/scenes/Settings/Details.js b/app/scenes/Settings/Details.js index 686d6d745..2c33a54ad 100644 --- a/app/scenes/Settings/Details.js +++ b/app/scenes/Settings/Details.js @@ -25,11 +25,14 @@ class Details extends React.Component { form: ?HTMLFormElement; @observable name: string; + @observable subdomain: ?string; @observable avatarUrl: ?string; componentDidMount() { - if (this.props.auth.team) { - this.name = this.props.auth.team.name; + const { team } = this.props.auth; + if (team) { + this.name = team.name; + this.subdomain = team.subdomain; } } @@ -40,23 +43,32 @@ class Details extends React.Component { handleSubmit = async (ev: SyntheticEvent<*>) => { ev.preventDefault(); - await this.props.auth.updateTeam({ - name: this.name, - avatarUrl: this.avatarUrl, - }); - this.props.ui.showToast('Settings saved', 'success'); + try { + await this.props.auth.updateTeam({ + name: this.name, + avatarUrl: this.avatarUrl, + subdomain: this.subdomain, + }); + this.props.ui.showToast('Settings saved', 'success'); + } catch (err) { + this.props.ui.showToast(err.message); + } }; handleNameChange = (ev: SyntheticInputEvent<*>) => { this.name = ev.target.value; }; + handleSubdomainChange = (ev: SyntheticInputEvent<*>) => { + this.subdomain = ev.target.value.toLowerCase(); + }; + handleAvatarUpload = (avatarUrl: string) => { this.avatarUrl = avatarUrl; }; handleAvatarError = (error: ?string) => { - this.props.ui.showToast(error || 'Unable to upload new avatar'); + this.props.ui.showToast(error || 'Unable to upload new logo'); }; get isValid() { @@ -72,18 +84,10 @@ class Details extends React.Component {

Details

- {team.slackConnected && ( - - This team is connected to a Slack team. Your - colleagues can join by signing in with their Slack account details. - - )} - {team.googleConnected && ( - - This team is connected to a Google domain. Your - colleagues can join by signing in with their Google account. - - )} + + These details affect the way that your Outline appears to everyone on + the team. + Logo @@ -104,11 +108,32 @@ class Details extends React.Component {
(this.form = ref)}> + {process.env.SUBDOMAINS_ENABLED && ( + + + {this.subdomain && ( + + Your knowledgebase will be accessed at{' '} + {this.subdomain}.getoutline.com + + )} + + )} diff --git a/app/stores/AuthStore.js b/app/stores/AuthStore.js index 02672538a..bafe11b82 100644 --- a/app/stores/AuthStore.js +++ b/app/stores/AuthStore.js @@ -2,8 +2,8 @@ import { observable, action, computed, autorun, runInAction } from 'mobx'; import invariant from 'invariant'; import Cookie from 'js-cookie'; -import localForage from 'localforage'; import { client } from 'utils/ApiClient'; +import { stripSubdomain } from 'shared/utils/domains'; import type { User, Team } from 'types'; const AUTH_STORE = 'AUTH_STORE'; @@ -102,8 +102,20 @@ class AuthStore { this.user = null; this.token = null; + // remove authentication token itself Cookie.remove('accessToken', { path: '/' }); - await localForage.clear(); + + // remove session record on apex cookie + const team = this.team; + if (team) { + const sessions = Cookie.getJSON('sessions') || {}; + delete sessions[team.subdomain || 'root']; + + Cookie.set('sessions', sessions, { + domain: stripSubdomain(window.location.hostname), + }); + this.team = null; + } // add a timestamp to force reload from server window.location.href = `${BASE_URL}?done=${new Date().getTime()}`; @@ -119,10 +131,7 @@ class AuthStore { } this.user = data.user; this.team = data.team; - - // load token from state for backwards compatability with - // sessions created pre-google auth - this.token = Cookie.get('accessToken') || data.token; + this.token = Cookie.get('accessToken'); if (this.token) setImmediate(() => this.fetch()); diff --git a/app/types/index.js b/app/types/index.js index 165827cd9..9daae0b83 100644 --- a/app/types/index.js +++ b/app/types/index.js @@ -46,6 +46,8 @@ export type Team = { slackConnected: boolean, googleConnected: boolean, sharing: boolean, + subdomain?: string, + url: string, }; export type NavigationNode = { diff --git a/app/utils/ApiClient.js b/app/utils/ApiClient.js index 56353b4aa..b57724020 100644 --- a/app/utils/ApiClient.js +++ b/app/utils/ApiClient.js @@ -1,5 +1,5 @@ // @flow -import _ from 'lodash'; +import { map } from 'lodash'; import invariant from 'invariant'; import stores from 'stores'; @@ -39,6 +39,8 @@ class ApiClient { const headers = new Headers({ Accept: 'application/json', 'Content-Type': 'application/json', + 'cache-control': 'no-cache', + pragma: 'no-cache', }); if (stores.auth.authenticated) { invariant(stores.auth.token, 'JWT token not set properly'); @@ -51,7 +53,8 @@ class ApiClient { body, headers, redirect: 'follow', - credentials: 'include', + credentials: 'omit', + cache: 'no-cache', }); if (response.status >= 200 && response.status < 300) { @@ -68,6 +71,14 @@ class ApiClient { const error = {}; error.statusCode = response.status; error.response = response; + + try { + const data = await response.json(); + error.message = data.message || ''; + } catch (_err) { + // we're trying to parse an error so JSON may not be valid + } + throw error; }; @@ -81,7 +92,7 @@ class ApiClient { // Helpers constructQueryString = (data: Object) => { - return _.map(data, (v, k) => { + return map(data, (v, k) => { return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`; }).join('&'); }; diff --git a/docker-compose.yml b/docker-compose.yml index 24259efa2..cc46862fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,7 @@ services: ports: - "3000:3000" volumes: - - .:/opt/outline + - .:/opt/outline:cached depends_on: - postgres - redis diff --git a/flow-typed/npm/koa_v2.x.x.js b/flow-typed/npm/koa_v2.x.x.js index 62f8db311..787c1fc94 100644 --- a/flow-typed/npm/koa_v2.x.x.js +++ b/flow-typed/npm/koa_v2.x.x.js @@ -14,17 +14,22 @@ declare module 'koa' { // Currently, import type doesn't work well ? // so copy `Server` from flow/lib/node.js#L820 declare class Server extends net$Server { - listen(port?: number, hostname?: string, backlog?: number, callback?: Function): Server, - listen(path: string, callback?: Function): Server, - listen(handle: Object, callback?: Function): Server, - close(callback?: Function): Server, - maxHeadersCount: number, - setTimeout(msecs: number, callback: Function): Server, - timeout: number, + listen( + port?: number, + hostname?: string, + backlog?: number, + callback?: Function + ): Server; + listen(path: string, callback?: Function): Server; + listen(handle: Object, callback?: Function): Server; + close(callback?: Function): Server; + maxHeadersCount: number; + setTimeout(msecs: number, callback: Function): Server; + timeout: number; } declare type ServerType = Server; - declare type JSON = | string | number | boolean | null | JSONObject | JSONArray; + declare type JSON = string | number | boolean | null | JSONObject | JSONArray; declare type JSONObject = { [key: string]: JSON }; declare type JSONArray = Array; @@ -34,11 +39,11 @@ declare module 'koa' { }; declare type RequestJSON = { - 'method': string, - 'url': string, - 'header': SimpleHeader, + method: string, + url: string, + header: SimpleHeader, }; - declare type RequestInspect = void|RequestJSON; + declare type RequestInspect = void | RequestJSON; declare type Request = { app: Application, req: http$IncomingMessage, @@ -60,7 +65,7 @@ declare module 'koa' { originalUrl: string, path: string, protocol: string, - query: {[key: string]: string}, // always string + query: { [key: string]: string }, // always string querystring: string, search: string, secure: boolean, // Shorthand for ctx.protocol == "https" to check if a request was issued via TLS. @@ -70,55 +75,55 @@ declare module 'koa' { type: string, url: string, - charset: string|void, - length: number|void, + charset: string | void, + length: number | void, -// Those functions comes from https://github.com/jshttp/accepts/blob/master/index.js -// request.js$L445 -// https://github.com/jshttp/accepts/blob/master/test/type.js - accepts: ((args: string[]) => string|false)& - // ToDo: There is an issue https://github.com/facebook/flow/issues/3009 - // if you meet some error here, temporarily add an additional annotation - // like: `request.accepts((['json', 'text']:Array))` to fix it. - ((arg: string, ...args: string[]) => string|false) & - ( () => string[] ) , // return the old value. + // Those functions comes from https://github.com/jshttp/accepts/blob/master/index.js + // request.js$L445 + // https://github.com/jshttp/accepts/blob/master/test/type.js + accepts: ((args: string[]) => string | false) & + // ToDo: There is an issue https://github.com/facebook/flow/issues/3009 + // if you meet some error here, temporarily add an additional annotation + // like: `request.accepts((['json', 'text']:Array))` to fix it. + ((arg: string, ...args: string[]) => string | false) & + (() => string[]), // return the old value. -// https://github.com/jshttp/accepts/blob/master/index.js#L153 -// https://github.com/jshttp/accepts/blob/master/test/charset.js - acceptsCharsets: ( (args: string[]) => buffer$Encoding|false)& - // ToDo: https://github.com/facebook/flow/issues/3009 - // if you meet some error here, see L70. - ( (arg: string, ...args: string[]) => buffer$Encoding|false ) & - ( () => string[] ), + // https://github.com/jshttp/accepts/blob/master/index.js#L153 + // https://github.com/jshttp/accepts/blob/master/test/charset.js + acceptsCharsets: ((args: string[]) => buffer$Encoding | false) & + // ToDo: https://github.com/facebook/flow/issues/3009 + // if you meet some error here, see L70. + ((arg: string, ...args: string[]) => buffer$Encoding | false) & + (() => string[]), -// https://github.com/jshttp/accepts/blob/master/index.js#L119 -// https://github.com/jshttp/accepts/blob/master/test/encoding.js - acceptsEncodings: ( (args: string[]) => string|false)& - // ToDo: https://github.com/facebook/flow/issues/3009 - // if you meet some error here, see L70. - ( (arg: string, ...args: string[]) => string|false ) & - ( () => string[] ), + // https://github.com/jshttp/accepts/blob/master/index.js#L119 + // https://github.com/jshttp/accepts/blob/master/test/encoding.js + acceptsEncodings: ((args: string[]) => string | false) & + // ToDo: https://github.com/facebook/flow/issues/3009 + // if you meet some error here, see L70. + ((arg: string, ...args: string[]) => string | false) & + (() => string[]), -// https://github.com/jshttp/accepts/blob/master/index.js#L185 -// https://github.com/jshttp/accepts/blob/master/test/language.js - acceptsLanguages: ( (args: string[]) => string|false) & - // ToDo: https://github.com/facebook/flow/issues/3009 - // if you meet some error here, see L70. - ( (arg: string, ...args: string[]) => string|false ) & - ( () => string[] ), + // https://github.com/jshttp/accepts/blob/master/index.js#L185 + // https://github.com/jshttp/accepts/blob/master/test/language.js + acceptsLanguages: ((args: string[]) => string | false) & + // ToDo: https://github.com/facebook/flow/issues/3009 + // if you meet some error here, see L70. + ((arg: string, ...args: string[]) => string | false) & + (() => string[]), get: (field: string) => string, -/* https://github.com/jshttp/type-is/blob/master/test/test.js + /* https://github.com/jshttp/type-is/blob/master/test/test.js * Check if the incoming request contains the "Content-Type" * header field, and it contains any of the give mime `type`s. * If there is no request body, `null` is returned. * If there is no content type, `false` is returned. * Otherwise, it returns the first `type` that matches. */ - is: ( (args: string[]) => null|false|string)& - ( (arg: string, ...args: string[]) => null|false|string ) & - ( () => string ), // should return the mime type + is: ((args: string[]) => null | false | string) & + ((arg: string, ...args: string[]) => null | false | string) & + (() => string), // should return the mime type toJSON: () => RequestJSON, inspect: () => RequestInspect, @@ -127,15 +132,15 @@ declare module 'koa' { }; declare type ResponseJSON = { - 'status': mixed, - 'message': mixed, - 'header': mixed, + status: mixed, + message: mixed, + header: mixed, }; declare type ResponseInspect = { - 'status': mixed, - 'message': mixed, - 'header': mixed, - 'body': mixed, + status: mixed, + message: mixed, + header: mixed, + body: mixed, }; declare type Response = { app: Application, @@ -145,7 +150,7 @@ declare module 'koa' { request: Request, // docs/api/response.md#L113. - body: string|Buffer|stream$Stream|Object|Array|null, // JSON contains null + body: string | Buffer | stream$Stream | Object | Array | null, // JSON contains null etag: string, header: SimpleHeader, headers: SimpleHeader, // alias as header @@ -160,21 +165,21 @@ declare module 'koa' { writable: boolean, // charset: string, // doesn't find in response.js - length: number|void, + length: number | void, append: (field: string, val: string | string[]) => void, attachment: (filename?: string) => void, get: (field: string) => string, // https://github.com/jshttp/type-is/blob/master/test/test.js // https://github.com/koajs/koa/blob/v2.x/lib/response.js#L382 - is: ( (arg: string[]) => false|string) & - ( (arg: string, ...args: string[]) => false|string ) & - ( () => string ), // should return the mime type + is: ((arg: string[]) => false | string) & + ((arg: string, ...args: string[]) => false | string) & + (() => string), // should return the mime type redirect: (url: string, alt?: string) => void, remove: (field: string) => void, // https://github.com/koajs/koa/blob/v2.x/lib/response.js#L418 - set: ((field: string, val: string | string[]) => void)& - ((field: {[key: string]: string | string[]}) => void), + set: ((field: string, val: string | string[]) => void) & + ((field: { [key: string]: string | string[] }) => void), vary: (field: string) => void, @@ -183,7 +188,7 @@ declare module 'koa' { inspect(): ResponseInspect, [key: string]: mixed, // props added by middlewares. - } + }; declare type ContextJSON = { request: RequestJSON, @@ -197,7 +202,7 @@ declare module 'koa' { // https://github.com/pillarjs/cookies declare type CookiesSetOptions = { domain: string, // domain of the cookie (no default). - maxAge: number, // milliseconds from Date.now() for expiry + maxAge?: number, // milliseconds from Date.now() for expiry expires?: Date, //cookie's expiration date (expires at the end of session by default). path?: string, // the path of the cookie (/ by default). secure?: boolean, // false by default for HTTP, true by default for HTTPS @@ -207,10 +212,14 @@ declare module 'koa' { overwrite?: boolean, // whether to overwrite previously set cookies of the same name (false by default). }; declare type Cookies = { - get: (name: string, options?: {signed: boolean}) => string|void, - set: ((name: string, value: string, options?: CookiesSetOptions) => Context)& - // delete cookie (an outbound header with an expired date is used.) - ( (name: string) => Context), + get: (name: string, options?: { signed: boolean }) => string | void, + set: (( + name: string, + value: string, + options?: CookiesSetOptions + ) => Context) & + // delete cookie (an outbound header with an expired date is used.) + ((name: string) => Context), }; // The default props of context come from two files // `application.createContext` & `context.js` @@ -228,12 +237,17 @@ declare module 'koa' { state: Object, // context.js#L55 - assert: (test: mixed, status: number, message?: string, opts?: mixed) => void, + assert: ( + test: mixed, + status: number, + message?: string, + opts?: mixed + ) => void, // context.js#L107 // if (!(err instanceof Error)) err = new Error(`non-error thrown: ${err}`); onerror: (err?: mixed) => void, // context.md#L88 - throw: ( status: number, msg?: string, opts?: Object) => void, + throw: (status: number, msg?: string, opts?: Object) => void, toJSON(): ContextJSON, inspect(): ContextJSON, @@ -287,32 +301,37 @@ declare module 'koa' { ip: $PropertyType, [key: string]: any, // props added by middlewares. - } + }; - declare type Middleware = - (ctx: Context, next: () => Promise) => Promise|void; + declare type Middleware = ( + ctx: Context, + next: () => Promise + ) => Promise | void; declare type ApplicationJSON = { - 'subdomainOffset': mixed, - 'proxy': mixed, - 'env': string, + subdomainOffset: mixed, + proxy: mixed, + env: string, }; declare class Application extends events$EventEmitter { - context: Context, + context: Context; // request handler for node's native http server. - callback: () => (req: http$IncomingMessage, res: http$ServerResponse) => void, - env: string, - keys?: Array|Object, // https://github.com/crypto-utils/keygrip - middleware: Array, - proxy: boolean, // when true proxy header fields will be trusted - request: Request, - response: Response, - server: Server, - subdomainOffset: number, + callback: () => ( + req: http$IncomingMessage, + res: http$ServerResponse + ) => void; + env: string; + keys?: Array | Object; // https://github.com/crypto-utils/keygrip + middleware: Array; + proxy: boolean; // when true proxy header fields will be trusted + request: Request; + response: Response; + server: Server; + subdomainOffset: number; - listen: $PropertyType, - toJSON(): ApplicationJSON, - inspect(): ApplicationJSON, - use(fn: Middleware): this, + listen: $PropertyType; + toJSON(): ApplicationJSON; + inspect(): ApplicationJSON; + use(fn: Middleware): this; } declare module.exports: Class; diff --git a/flow-typed/npm/localforage_v1.5.x.js b/flow-typed/npm/localforage_v1.5.x.js deleted file mode 100644 index bcbf1704b..000000000 --- a/flow-typed/npm/localforage_v1.5.x.js +++ /dev/null @@ -1,84 +0,0 @@ -// flow-typed signature: 37b164ad4c10b3c89887d1fd5b7ca096 -// flow-typed version: 9c854fa980/localforage_v1.5.x/flow_>=v0.25.x - -type PartialConfig = { - driver?: string | Array, - name?: string, - size?: number, - storeName?: string, - version?: string, - description?: string, -}; - -type Driver = { - _driver: string, - _initStorage(config: PartialConfig): void, - - getItem( - key: string, - successCallback?: (err?: Error, value?: T) => mixed, - ): ?Promise, - setItem( - key: string, - value: T, - successCallback?: (err?: Error, value?: T) => mixed, - ): ?Promise, - removeItem( - key: string, - successCallback?: (err?: Error) => mixed, - ): ?Promise, - clear(successCallback?: ?(numberOfKeys: number) => mixed): ?Promise, - length(successCallback?: (numberOfKeys: number) => mixed): ?Promise, - key( - keyIndex: number, - successCallback?: (keyName: string) => mixed, - ): ?Promise, - keys( - successCallback?: (keyNames: Array) => mixed, - ): ?Promise>, -}; - -type localforageInstance = { - INDEXEDDB: 'asyncStorage', - WEBSQL: 'webSQLStorage', - LOCALSTORAGE: 'localStorageWrapper', - - getItem( - key: string, - successCallback?: (err?: Error, value?: T) => mixed, - ): Promise, - setItem( - key: string, - value: T, - successCallback?: (err?: Error, value?: T) => mixed, - ): Promise, - removeItem( - key: string, - successCallback?: (err?: Error) => mixed, - ): Promise, - clear(successCallback?: ?(numberOfKeys: number) => mixed): Promise, - length(successCallback?: (numberOfKeys: number) => mixed): Promise, - key( - keyIndex: number, - successCallback?: (keyName: string) => mixed, - ): Promise, - keys( - successCallback?: (keyNames: Array) => mixed, - ): Promise>, - iterate( - iteratorCallback: (value: T, key: string, iterationNumber: number) => mixed, - successCallback?: (result: void | [string, T]) => mixed, - ): Promise, - setDriver(driverNames: string | Array): void, - config(config?: PartialConfig): boolean | PartialConfig, - defineDriver(driver: Driver): void, - driver(): string, - ready(): Promise, - supports(driverName: string): boolean, - createInstance(config?: PartialConfig): localforageInstance, - dropInstance(config?: PartialConfig): Promise, -}; - -declare module 'localforage' { - declare module.exports: localforageInstance; -} diff --git a/package.json b/package.json index a6d52ddc9..95c901d11 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "shared" ], "moduleNameMapper": { + "^shared/(.*)$": "/shared/$1", "^.*[.](s?css|css)$": "/__mocks__/styleMock.js", "^.*[.](gif|ttf|eot|svg)$": "/__test__/fileMock.js" }, @@ -128,7 +129,6 @@ "koa-sendfile": "2.0.0", "koa-static": "^4.0.1", "koa-sslify": "2.1.2", - "localforage": "^1.5.0", "lodash": "^4.17.4", "mobx": "^3.1.9", "mobx-react": "^4.1.8", @@ -139,6 +139,7 @@ "normalizr": "2.0.1", "outline-icons": "^1.3.2", "oy-vey": "^0.10.0", + "parse-domain": "2.1.6", "pg": "^6.1.5", "pg-hstore": "2.3.2", "polished": "1.2.1", diff --git a/server/api/hooks.test.js b/server/api/hooks.test.js index 27b7a4151..117bc9bb1 100644 --- a/server/api/hooks.test.js +++ b/server/api/hooks.test.js @@ -38,7 +38,7 @@ describe('#hooks.unfurl', async () => { links: [ { domain: 'getoutline.com', - url: document.getUrl(), + url: document.url, }, ], }, diff --git a/server/api/team.js b/server/api/team.js index 3bc4a1b8a..ab62d7059 100644 --- a/server/api/team.js +++ b/server/api/team.js @@ -12,13 +12,17 @@ const { authorize } = policy; const router = new Router(); router.post('team.update', auth(), async ctx => { - const { name, avatarUrl, sharing } = ctx.body; + const { name, avatarUrl, subdomain, sharing } = ctx.body; const endpoint = publicS3Endpoint(); const user = ctx.state.user; const team = await Team.findById(user.teamId); authorize(user, 'update', team); + if (process.env.SUBDOMAINS_ENABLED === 'true') { + team.subdomain = subdomain === '' ? null : subdomain; + } + if (name) team.name = name; if (sharing !== undefined) team.sharing = sharing; if (avatarUrl && avatarUrl.startsWith(`${endpoint}/uploads/${user.id}`)) { diff --git a/server/auth/google.js b/server/auth/google.js index c8f28f51e..6c00ef3cb 100644 --- a/server/auth/google.js +++ b/server/auth/google.js @@ -1,10 +1,10 @@ // @flow import crypto from 'crypto'; import Router from 'koa-router'; -import addMonths from 'date-fns/add_months'; import { capitalize } from 'lodash'; import { OAuth2Client } from 'google-auth-library'; import { User, Team } from '../models'; +import auth from '../middlewares/authentication'; const router = new Router(); const client = new OAuth2Client( @@ -29,7 +29,7 @@ router.get('google', async ctx => { }); // signin callback from Google -router.get('google.callback', async ctx => { +router.get('google.callback', auth({ required: false }), async ctx => { const { code } = ctx.request.query; ctx.assertPresent(code, 'code is required'); const response = await client.getToken(code); @@ -52,7 +52,8 @@ router.get('google.callback', async ctx => { } const googleId = profile.data.hd; - const teamName = capitalize(profile.data.hd.split('.')[0]); + const hostname = profile.data.hd.split('.')[0]; + const teamName = capitalize(hostname); // attempt to get logo from Clearbit API. If one doesn't exist then // fall back to using tiley to generate a placeholder logo @@ -91,22 +92,12 @@ router.get('google.callback', async ctx => { }); if (isFirstUser) { - await team.createFirstCollection(user.id); + await team.provisionFirstCollection(user.id); + await team.provisionSubdomain(hostname); } - // not awaiting the promise here so that the request is not blocked - user.updateSignedIn(ctx.request.ip); - - ctx.cookies.set('lastSignedIn', 'google', { - httpOnly: false, - expires: new Date('2100'), - }); - ctx.cookies.set('accessToken', user.getJwtToken(), { - httpOnly: false, - expires: addMonths(new Date(), 1), - }); - - ctx.redirect('/'); + // set cookies on response and redirect to team subdomain + ctx.signIn(user, team, 'google'); }); export default router; diff --git a/server/auth/index.js b/server/auth/index.js index ed416e76a..37317c823 100644 --- a/server/auth/index.js +++ b/server/auth/index.js @@ -3,18 +3,40 @@ import bodyParser from 'koa-bodyparser'; import Koa from 'koa'; import Router from 'koa-router'; import validation from '../middlewares/validation'; +import auth from '../middlewares/authentication'; +import addMonths from 'date-fns/add_months'; +import { Team } from '../models'; +import { stripSubdomain } from '../../shared/utils/domains'; import slack from './slack'; import google from './google'; -const auth = new Koa(); +const app = new Koa(); const router = new Router(); router.use('/', slack.routes()); router.use('/', google.routes()); -auth.use(bodyParser()); -auth.use(validation()); -auth.use(router.routes()); +router.get('/redirect', auth(), async ctx => { + const user = ctx.state.user; -export default auth; + // transfer access token cookie from root to subdomain + ctx.cookies.set('accessToken', undefined, { + httpOnly: true, + domain: stripSubdomain(ctx.request.hostname), + }); + + ctx.cookies.set('accessToken', user.getJwtToken(), { + httpOnly: false, + expires: addMonths(new Date(), 3), + }); + + const team = await Team.findById(user.teamId); + ctx.redirect(`${team.url}/dashboard`); +}); + +app.use(bodyParser()); +app.use(validation()); +app.use(router.routes()); + +export default app; diff --git a/server/auth/slack.js b/server/auth/slack.js index ecfa562ef..45ee15f02 100644 --- a/server/auth/slack.js +++ b/server/auth/slack.js @@ -2,7 +2,7 @@ import Router from 'koa-router'; import auth from '../middlewares/authentication'; import addHours from 'date-fns/add_hours'; -import addMonths from 'date-fns/add_months'; +import { stripSubdomain } from '../../shared/utils/domains'; import { slackAuth } from '../../shared/utils/routeHelpers'; import { Authentication, Integration, User, Team } from '../models'; import * as Slack from '../slack'; @@ -18,18 +18,19 @@ router.get('slack', async ctx => { ctx.cookies.set('state', state, { httpOnly: false, expires: addHours(new Date(), 1), + domain: stripSubdomain(ctx.request.hostname), }); ctx.redirect(slackAuth(state)); }); // signin callback from Slack -router.get('slack.callback', async ctx => { +router.get('slack.callback', auth({ required: false }), async ctx => { const { code, error, state } = ctx.request.query; ctx.assertPresent(code || error, 'code is required'); ctx.assertPresent(state, 'state is required'); if (state !== ctx.cookies.get('state') || error) { - ctx.redirect('/?notice=auth-error'); + ctx.redirect(`/?notice=auth-error`); return; } @@ -60,22 +61,12 @@ router.get('slack.callback', async ctx => { }); if (isFirstUser) { - await team.createFirstCollection(user.id); + await team.provisionFirstCollection(user.id); + await team.provisionSubdomain(data.team.domain); } - // not awaiting the promise here so that the request is not blocked - user.updateSignedIn(ctx.request.ip); - - ctx.cookies.set('lastSignedIn', 'slack', { - httpOnly: false, - expires: new Date('2100'), - }); - ctx.cookies.set('accessToken', user.getJwtToken(), { - httpOnly: false, - expires: addMonths(new Date(), 1), - }); - - ctx.redirect('/'); + // set cookies on response and redirect to team subdomain + ctx.signIn(user, team, 'slack'); }); router.get('slack.commands', auth(), async ctx => { diff --git a/server/mailer.test.js b/server/mailer.test.js index fe2023628..2fc77f97a 100644 --- a/server/mailer.test.js +++ b/server/mailer.test.js @@ -6,7 +6,9 @@ describe('Mailer', () => { let sendMailOutput; beforeEach(() => { + process.env.URL = 'http://localhost:3000'; process.env.SMTP_FROM_EMAIL = 'hello@example.com'; + jest.resetModules(); fakeMailer = new Mailer(); fakeMailer.transporter = { diff --git a/server/middlewares/subdomainRedirect.js b/server/middlewares/apexRedirect.js similarity index 54% rename from server/middlewares/subdomainRedirect.js rename to server/middlewares/apexRedirect.js index 2ae238ffb..616518444 100644 --- a/server/middlewares/subdomainRedirect.js +++ b/server/middlewares/apexRedirect.js @@ -1,10 +1,10 @@ // @flow -import { type Context } from 'koa'; +import type { Context } from 'koa'; -export default function subdomainRedirect() { - return async function subdomainRedirectMiddleware( +export default function apexRedirect() { + return async function apexRedirectMiddleware( ctx: Context, - next: () => Promise + next: () => Promise<*> ) { if (ctx.headers.host === 'getoutline.com') { ctx.redirect(`https://www.${ctx.headers.host}${ctx.path}`); diff --git a/server/middlewares/authentication.js b/server/middlewares/authentication.js index 67f570ef1..eeea09711 100644 --- a/server/middlewares/authentication.js +++ b/server/middlewares/authentication.js @@ -3,6 +3,9 @@ import JWT from 'jsonwebtoken'; import { type Context } from 'koa'; import { User, ApiKey } from '../models'; import { AuthenticationError, UserSuspendedError } from '../errors'; +import addMonths from 'date-fns/add_months'; +import addMinutes from 'date-fns/add_minutes'; +import { stripSubdomain } from '../../shared/utils/domains'; export default function auth(options?: { required?: boolean } = {}) { return async function authMiddleware(ctx: Context, next: () => Promise<*>) { @@ -28,7 +31,7 @@ export default function auth(options?: { required?: boolean } = {}) { token = ctx.body.token; } else if (ctx.request.query.token) { token = ctx.request.query.token; - } else if (ctx.cookies.get('accessToken')) { + } else { token = ctx.cookies.get('accessToken'); } @@ -90,6 +93,56 @@ export default function auth(options?: { required?: boolean } = {}) { ctx.cache[user.id] = user; } + ctx.signIn = (user, team, service) => { + // update the database when the user last signed in + user.updateSignedIn(ctx.request.ip); + + const domain = stripSubdomain(ctx.request.hostname); + const expires = addMonths(new Date(), 3); + + // set a cookie for which service we last signed in with. This is + // only used to display a UI hint for the user for next time + ctx.cookies.set('lastSignedIn', service, { + httpOnly: false, + expires: new Date('2100'), + domain, + }); + + // set a transfer cookie for the access token itself and redirect + // to the teams subdomain if subdomains are enabled + if (process.env.SUBDOMAINS_ENABLED === 'true' && team.subdomain) { + // get any existing sessions (teams signed in) and add this team + const existing = JSON.parse(ctx.cookies.get('sessions') || '{}'); + const sessions = JSON.stringify({ + ...existing, + [team.subdomain]: { + name: team.name, + logoUrl: team.logoUrl, + url: team.url, + expires, + }, + }); + ctx.cookies.set('sessions', sessions, { + httpOnly: false, + expires, + domain, + }); + + ctx.cookies.set('accessToken', user.getJwtToken(), { + httpOnly: true, + expires: addMinutes(new Date(), 1), + domain, + }); + ctx.redirect(`${team.url}/auth/redirect`); + } else { + ctx.cookies.set('accessToken', user.getJwtToken(), { + httpOnly: false, + expires, + }); + ctx.redirect(`${team.url}/dashboard`); + } + }; + return next(); }; } diff --git a/server/migrations/20181031015046-add-subdomain-to-team.js b/server/migrations/20181031015046-add-subdomain-to-team.js new file mode 100644 index 000000000..75f805a12 --- /dev/null +++ b/server/migrations/20181031015046-add-subdomain-to-team.js @@ -0,0 +1,14 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('teams', 'subdomain', { + type: Sequelize.STRING, + allowNull: true, + unique: true + }); + await queryInterface.addIndex('teams', ['subdomain']); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('teams', 'subdomain'); + await queryInterface.removeIndex('teams', ['subdomain']); + } +} \ No newline at end of file diff --git a/server/models/Collection.js b/server/models/Collection.js index 795127006..d9bfaa8eb 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -70,6 +70,11 @@ const Collection = sequelize.define( await collection.save(); }, }, + getterMethods: { + url() { + return `/collections/${this.id}`; + }, + }, } ); @@ -120,10 +125,6 @@ Collection.addHook('afterUpdate', model => // Instance methods -Collection.prototype.getUrl = function() { - return `/collections/${this.id}`; -}; - Collection.prototype.addDocumentToStructure = async function( document, index, diff --git a/server/models/Collection.test.js b/server/models/Collection.test.js index 930ce991a..65d8e68ce 100644 --- a/server/models/Collection.test.js +++ b/server/models/Collection.test.js @@ -6,10 +6,10 @@ import uuid from 'uuid'; beforeEach(flushdb); beforeEach(jest.resetAllMocks); -describe('#getUrl', () => { +describe('#url', () => { test('should return correct url for the collection', () => { const collection = new Collection({ id: '1234' }); - expect(collection.getUrl()).toBe('/collections/1234'); + expect(collection.url).toBe('/collections/1234'); }); }); diff --git a/server/models/Document.js b/server/models/Document.js index 62cb5ae71..2cca85179 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -104,6 +104,12 @@ const Document = sequelize.define( afterCreate: createRevision, afterUpdate: createRevision, }, + getterMethods: { + url: function() { + const slugifiedTitle = slugify(this.title); + return `/doc/${slugifiedTitle}-${this.urlId}`; + }, + }, } ); @@ -312,18 +318,13 @@ Document.prototype.getSummary = function() { return lines.length >= 1 ? lines[1] : ''; }; -Document.prototype.getUrl = function() { - const slugifiedTitle = slugify(this.title); - return `/doc/${slugifiedTitle}-${this.urlId}`; -}; - Document.prototype.toJSON = function() { // Warning: only use for new documents as order of children is // handled in the collection's documentStructure return { id: this.id, title: this.title, - url: this.getUrl(), + url: this.url, children: [], }; }; diff --git a/server/models/Team.js b/server/models/Team.js index 933303854..5d1edba53 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -1,7 +1,9 @@ // @flow import uuid from 'uuid'; +import { URL } from 'url'; import { DataTypes, sequelize, Op } from '../sequelize'; import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3'; +import { RESERVED_SUBDOMAINS } from '../../shared/utils/domains'; import Collection from './Collection'; import User from './User'; @@ -14,6 +16,26 @@ const Team = sequelize.define( primaryKey: true, }, name: DataTypes.STRING, + subdomain: { + type: DataTypes.STRING, + allowNull: true, + validate: { + isLowercase: true, + is: { + args: [/^[a-z\d-]+$/, 'i'], + msg: 'Must be only alphanumeric and dashes', + }, + len: { + args: [4, 32], + msg: 'Must be between 4 and 32 characters', + }, + notIn: { + args: [RESERVED_SUBDOMAINS], + msg: 'You chose a restricted word, please try another.', + }, + }, + unique: true, + }, slackId: { type: DataTypes.STRING, allowNull: true }, googleId: { type: DataTypes.STRING, allowNull: true }, avatarUrl: { type: DataTypes.STRING, allowNull: true }, @@ -21,12 +43,22 @@ const Team = sequelize.define( slackData: DataTypes.JSONB, }, { - indexes: [ - { - unique: true, - fields: ['slackId'], + getterMethods: { + url() { + if (!this.subdomain || process.env.SUBDOMAINS_ENABLED !== 'true') { + return process.env.URL; + } + + const url = new URL(process.env.URL); + url.host = `${this.subdomain}.${url.host}`; + return url.href.replace(/\/$/, ''); }, - ], + logoUrl() { + return ( + this.avatarUrl || (this.slackData ? this.slackData.image_88 : null) + ); + }, + }, } ); @@ -53,7 +85,24 @@ const uploadAvatar = async model => { } }; -Team.prototype.createFirstCollection = async function(userId) { +Team.prototype.provisionSubdomain = async function(subdomain) { + if (this.subdomain) return this.subdomain; + + let append = 0; + while (true) { + try { + await this.update({ subdomain }); + break; + } catch (err) { + // subdomain was invalid or already used, try again + subdomain = `${subdomain}${++append}`; + } + } + + return subdomain; +}; + +Team.prototype.provisionFirstCollection = async function(userId) { return await Collection.create({ name: 'General', description: 'Your first Collection', diff --git a/server/models/Team.test.js b/server/models/Team.test.js new file mode 100644 index 000000000..4563bc323 --- /dev/null +++ b/server/models/Team.test.js @@ -0,0 +1,28 @@ +/* eslint-disable flowtype/require-valid-file-annotation */ +import { flushdb } from '../test/support'; +import { buildTeam } from '../test/factories'; + +beforeEach(flushdb); + +it('should set subdomain if available', async () => { + const team = await buildTeam(); + const subdomain = await team.provisionSubdomain('testy'); + expect(subdomain).toEqual('testy'); + expect(team.subdomain).toEqual('testy'); +}); + +it('should set subdomain with append if unavailable', async () => { + await buildTeam({ subdomain: 'myteam' }); + + const team = await buildTeam(); + const subdomain = await team.provisionSubdomain('myteam'); + expect(subdomain).toEqual('myteam1'); + expect(team.subdomain).toEqual('myteam1'); +}); + +it('should do nothing if subdomain already set', async () => { + const team = await buildTeam({ subdomain: 'example' }); + const subdomain = await team.provisionSubdomain('myteam'); + expect(subdomain).toEqual('example'); + expect(team.subdomain).toEqual('example'); +}); diff --git a/server/pages/Home.js b/server/pages/Home.js index 685198b97..a7f49386d 100644 --- a/server/pages/Home.js +++ b/server/pages/Home.js @@ -4,8 +4,9 @@ import { Helmet } from 'react-helmet'; import styled from 'styled-components'; import Grid from 'styled-components-grid'; import breakpoint from 'styled-components-breakpoint'; -import Notice from '../../shared/components/Notice'; +import AuthErrors from './components/AuthErrors'; import Hero from './components/Hero'; +import HeroText from './components/HeroText'; import Centered from './components/Centered'; import SigninButtons from './components/SigninButtons'; import SlackLogo from '../../shared/components/SlackLogo'; @@ -36,24 +37,7 @@ function Home(props: Props) {

- {props.notice === 'google-hd' && ( - - Sorry, Google sign in cannot be used with a personal email. Please - try signing in with your company Google account. - - )} - {props.notice === 'hd-not-allowed' && ( - - Sorry, your Google apps domain is not allowed. Please try again - with an allowed company domain. - - )} - {props.notice === 'auth-error' && ( - - Authentication failed - we were unable to sign you in at this - time. Please try again. - - )} + @@ -80,7 +64,7 @@ function Home(props: Props) { @@ -232,13 +216,4 @@ const Footer = styled.div` `}; `; -const HeroText = styled.p` - font-size: 22px; - color: #666; - font-weight: 500; - text-align: left; - max-width: 600px; - margin-bottom: 2em; -`; - export default Home; diff --git a/server/pages/SubdomainSignin.js b/server/pages/SubdomainSignin.js new file mode 100644 index 000000000..a63ecddf4 --- /dev/null +++ b/server/pages/SubdomainSignin.js @@ -0,0 +1,81 @@ +// @flow +import * as React from 'react'; +import { Helmet } from 'react-helmet'; +import styled from 'styled-components'; +import Grid from 'styled-components-grid'; +import Hero from './components/Hero'; +import HeroText from './components/HeroText'; +import SigninButtons from './components/SigninButtons'; +import AuthErrors from './components/AuthErrors'; +import Centered from './components/Centered'; +import { Team } from '../models'; + +type Props = { + team: Team, + notice?: 'google-hd' | 'auth-error' | 'hd-not-allowed', + lastSignedIn: string, + googleSigninEnabled: boolean, + slackSigninEnabled: boolean, + hostname: string, +}; + +function SubdomainSignin({ + team, + lastSignedIn, + notice, + googleSigninEnabled, + slackSigninEnabled, + hostname, +}: Props) { + googleSigninEnabled = !!team.googleId && googleSigninEnabled; + slackSigninEnabled = !!team.slackId && slackSigninEnabled; + + // only show the "last signed in" hint if there is more than one option available + const signinHint = + googleSigninEnabled && slackSigninEnabled ? lastSignedIn : undefined; + + return ( + + + Outline - Sign in to {team.name} + + + +

{lastSignedIn ? 'Welcome back,' : 'Hey there,'}

+ + Sign in with your team account to continue to {team.name}. + {hostname} + +

+ +

+ +
+
+ +

+ Trying to create or sign in to a different team?{' '} + Head to the homepage. +

+
+
+ ); +} + +const Subdomain = styled.span` + display: block; + font-weight: 500; + font-size: 16px; + margin-top: 0; +`; + +const Alternative = styled(Centered)` + padding: 2em 0; + text-align: center; +`; + +export default SubdomainSignin; diff --git a/server/pages/components/AuthErrors.js b/server/pages/components/AuthErrors.js new file mode 100644 index 000000000..6a6cf80cd --- /dev/null +++ b/server/pages/components/AuthErrors.js @@ -0,0 +1,32 @@ +// @flow +import * as React from 'react'; +import Notice from '../../../shared/components/Notice'; + +type Props = { + notice?: string, +}; + +export default function AuthErrors({ notice }: Props) { + return ( + + {notice === 'google-hd' && ( + + Sorry, Google sign in cannot be used with a personal email. Please try + signing in with your company Google account. + + )} + {notice === 'hd-not-allowed' && ( + + Sorry, your Google apps domain is not allowed. Please try again with + an allowed company domain. + + )} + {notice === 'auth-error' && ( + + Authentication failed - we were unable to sign you in at this time. + Please try again. + + )} + + ); +} diff --git a/server/pages/components/Hero.js b/server/pages/components/Hero.js index c1cc4116c..afb7e6250 100644 --- a/server/pages/components/Hero.js +++ b/server/pages/components/Hero.js @@ -11,6 +11,11 @@ const Hero = styled(Centered)` font-size: 3.5em; line-height: 1em; } + + h2 { + font-size: 2.5em; + line-height: 1em; + } `; export default Hero; diff --git a/server/pages/components/HeroText.js b/server/pages/components/HeroText.js new file mode 100644 index 000000000..9d65e1946 --- /dev/null +++ b/server/pages/components/HeroText.js @@ -0,0 +1,13 @@ +// @flow +import styled from 'styled-components'; + +const HeroText = styled.p` + font-size: 22px; + color: #666; + font-weight: 500; + text-align: left; + max-width: 600px; + margin-bottom: 2em; +`; + +export default HeroText; diff --git a/server/pages/components/Layout.js b/server/pages/components/Layout.js index addec1d4c..3b913d9d0 100644 --- a/server/pages/components/Layout.js +++ b/server/pages/components/Layout.js @@ -15,9 +15,11 @@ export const screenshotUrl = `${process.env.URL}/screenshot.png`; type Props = { children?: React.Node, + sessions: Object, + loggedIn: boolean, }; -function Layout({ children }: Props) { +function Layout({ children, loggedIn, sessions }: Props) { return ( @@ -65,7 +67,7 @@ function Layout({ children }: Props) { {'{{CSS}}'} - + {children} diff --git a/server/pages/components/Navigation.js b/server/pages/components/Navigation.js index 7458563bf..5fd16da2f 100644 --- a/server/pages/components/Navigation.js +++ b/server/pages/components/Navigation.js @@ -1,11 +1,15 @@ // @flow import * as React from 'react'; +import { sortBy } from 'lodash'; import styled from 'styled-components'; import breakpoint from 'styled-components-breakpoint'; import Centered from './Centered'; +import TeamLogo from '../../../shared/components/TeamLogo'; +import { fadeAndScaleIn } from '../../../shared/styles/animations'; import { developers, changelog, + features, about, privacy, githubUrl, @@ -13,13 +17,29 @@ import { spectrumUrl, } from '../../../shared/utils/routeHelpers'; -function TopNavigation() { +type Sessions = { + [subdomain: string]: { + name: string, + logoUrl: string, + expires: string, + url: string, + }, +}; + +type Props = { + sessions: ?Sessions, + loggedIn: boolean, +}; + +function TopNavigation({ sessions, loggedIn }: Props) { + const orderedSessions = sortBy(sessions, 'name'); + return ( ); @@ -74,13 +122,8 @@ const MenuLinkStyle = props => ` } `; -const Menu = styled.ul` - margin: 0; - padding: 0; - list-style: none; -`; - const MenuItem = styled.li` + position: relative; display: inline-block; margin: 0 0 0 40px; @@ -89,6 +132,34 @@ const MenuItem = styled.li` } ${MenuLinkStyle}; + + ${props => + props.highlighted && + ` + position: relative; + border: 2px solid ${props.theme.slate}; + border-radius: 4px; + padding: 6px 8px; + margin-top: -6px; + margin-bottom: -6px; + + &:hover { + border: 2px solid ${props.theme.slateDark}; + + > a { + color: ${props.theme.slateDark}; + } + } + + > a:hover { + text-decoration: none; + } + `}; + + &:hover ol { + animation: ${fadeAndScaleIn} 200ms ease; + display: block; + } `; const MenuItemDesktop = styled(MenuItem)` @@ -99,6 +170,42 @@ const MenuItemDesktop = styled(MenuItem)` `}; `; +const Menu = styled.ul` + margin: 0; + padding: 0; + list-style: none; + + ol { + display: none; + position: absolute; + margin: 0; + padding: 0; + right: 0; + top: 34px; + + background: #fff; + border-radius: 4px; + min-width: 160px; + padding: 0 0.5em; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 8px rgba(0, 0, 0, 0.08), + 0 2px 4px rgba(0, 0, 0, 0.08); + + ${MenuItem} { + padding: 0.5em 0; + margin: 0; + } + + ${MenuItem} a { + display: flex; + align-items: center; + } + + ${TeamLogo} { + margin-right: 0.5em; + } + } +`; + const Nav = styled(Centered)` display: flex; padding: 20px 0; diff --git a/server/pages/components/SigninButtons.js b/server/pages/components/SigninButtons.js index 74ae660db..759019aa0 100644 --- a/server/pages/components/SigninButtons.js +++ b/server/pages/components/SigninButtons.js @@ -8,7 +8,7 @@ import SlackLogo from '../../../shared/components/SlackLogo'; import breakpoint from 'styled-components-breakpoint'; type Props = { - lastSignedIn: string, + lastSignedIn?: string, googleSigninEnabled: boolean, slackSigninEnabled: boolean, }; diff --git a/server/presenters/collection.js b/server/presenters/collection.js index 5b9af99da..f4e9cd95f 100644 --- a/server/presenters/collection.js +++ b/server/presenters/collection.js @@ -23,7 +23,7 @@ async function present(ctx: Object, collection: Collection) { const data = { id: collection.id, - url: collection.getUrl(), + url: collection.url, name: collection.name, description: collection.description, color: collection.color || '#4E5C6E', diff --git a/server/presenters/document.js b/server/presenters/document.js index e84d7704f..50388e657 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -25,7 +25,7 @@ async function present(ctx: Object, document: Document, options: ?Options) { const data = { id: document.id, - url: document.getUrl(), + url: document.url, urlId: document.urlId, title: document.title, text: document.text, diff --git a/server/presenters/share.js b/server/presenters/share.js index 1de3825ca..8d9e5f7df 100644 --- a/server/presenters/share.js +++ b/server/presenters/share.js @@ -6,7 +6,7 @@ function present(ctx: Object, share: Share) { return { id: share.id, documentTitle: share.document.title, - documentUrl: share.document.getUrl(), + documentUrl: share.document.url, url: `${process.env.URL}/share/${share.id}`, createdBy: presentUser(ctx, share.user), createdAt: share.createdAt, diff --git a/server/presenters/slackAttachment.js b/server/presenters/slackAttachment.js index a8f842126..5c91a9bfe 100644 --- a/server/presenters/slackAttachment.js +++ b/server/presenters/slackAttachment.js @@ -11,7 +11,7 @@ function present(document: Document, context?: string) { return { color: document.collection.color, title: document.title, - title_link: `${process.env.URL}${document.getUrl()}`, + title_link: `${process.env.URL}${document.url}`, footer: document.collection.name, text, ts: document.getTimestamp(), diff --git a/server/presenters/team.js b/server/presenters/team.js index a600ac2cb..7180d5aa6 100644 --- a/server/presenters/team.js +++ b/server/presenters/team.js @@ -7,11 +7,12 @@ function present(ctx: Object, team: Team) { return { id: team.id, name: team.name, - avatarUrl: - team.avatarUrl || (team.slackData ? team.slackData.image_88 : null), + avatarUrl: team.logoUrl, slackConnected: !!team.slackId, googleConnected: !!team.googleId, sharing: team.sharing, + subdomain: team.subdomain, + url: team.url, }; } diff --git a/server/routes.js b/server/routes.js index bf50d5ab4..cd979f125 100644 --- a/server/routes.js +++ b/server/routes.js @@ -5,10 +5,12 @@ import Koa from 'koa'; import Router from 'koa-router'; import sendfile from 'koa-sendfile'; import serve from 'koa-static'; -import subdomainRedirect from './middlewares/subdomainRedirect'; +import parseDomain from 'parse-domain'; +import apexRedirect from './middlewares/apexRedirect'; import renderpage from './utils/renderpage'; import { robotsResponse } from './utils/robots'; import { NotFoundError } from './errors'; +import { Team } from './models'; import Home from './pages/Home'; import About from './pages/About'; @@ -16,6 +18,7 @@ import Changelog from './pages/Changelog'; import Privacy from './pages/Privacy'; import Pricing from './pages/Pricing'; import Api from './pages/Api'; +import SubdomainSignin from './pages/SubdomainSignin'; const isProduction = process.env.NODE_ENV === 'production'; const koa = new Koa(); @@ -63,21 +66,47 @@ router.get('/changelog', async ctx => { // home page router.get('/', async ctx => { const lastSignedIn = ctx.cookies.get('lastSignedIn'); + const domain = parseDomain(ctx.request.hostname); + const subdomain = domain ? domain.subdomain : undefined; const accessToken = ctx.cookies.get('accessToken'); + ctx.set('Cache-Control', 'no-cache'); + if (accessToken) { - await renderapp(ctx); - } else { - await renderpage( - ctx, - - ); + return renderapp(ctx); } + + if (subdomain) { + const team = await Team.find({ + where: { subdomain }, + }); + if (team && process.env.SUBDOMAINS_ENABLED) { + return renderpage( + ctx, + + ); + } + + ctx.redirect(`${process.env.URL}?notice=invalid-auth`); + return; + } + + return renderpage( + ctx, + + ); }); // Other @@ -90,7 +119,7 @@ router.get('*', async ctx => { }); // middleware -koa.use(subdomainRedirect()); +koa.use(apexRedirect()); koa.use(router.routes()); export default koa; diff --git a/server/services/slack/index.js b/server/services/slack/index.js index 6903b9c9a..08b225244 100644 --- a/server/services/slack/index.js +++ b/server/services/slack/index.js @@ -48,7 +48,7 @@ class Slack { { color: collection.color, title: collection.name, - title_link: `${process.env.URL}${collection.getUrl()}`, + title_link: `${process.env.URL}${collection.url}`, text: collection.description, }, ], diff --git a/server/utils/renderpage.js b/server/utils/renderpage.js index ed57c8ace..c9fe1be6e 100644 --- a/server/utils/renderpage.js +++ b/server/utils/renderpage.js @@ -13,10 +13,17 @@ import theme from '../../shared/styles/theme'; const sheet = new ServerStyleSheet(); export default function renderpage(ctx: Object, children: React.Node) { + const sessions = JSON.parse(ctx.cookies.get('sessions') || '{}'); + const loggedIn = !!( + ctx.cookies.get('accessToken') || Object.keys(sessions).length + ); + const html = ReactDOMServer.renderToString( - {children} + + {children} + ); diff --git a/setupJest.js b/setupJest.js index ac3a3fe1f..98896a9cf 100644 --- a/setupJest.js +++ b/setupJest.js @@ -10,4 +10,4 @@ const snap = children => { }; global.localStorage = localStorage; -global.snap = snap; +global.snap = snap; \ No newline at end of file diff --git a/app/components/Sidebar/components/TeamLogo.js b/shared/components/TeamLogo.js similarity index 100% rename from app/components/Sidebar/components/TeamLogo.js rename to shared/components/TeamLogo.js diff --git a/shared/utils/domains.js b/shared/utils/domains.js new file mode 100644 index 000000000..c9c3629f2 --- /dev/null +++ b/shared/utils/domains.js @@ -0,0 +1,66 @@ +// @flow +import parseDomain from 'parse-domain'; + +export function stripSubdomain(hostname: string) { + const parsed = parseDomain(hostname); + if (!parsed) return hostname; + + if (parsed.tld) return `${parsed.domain}.${parsed.tld}`; + return parsed.domain; +} + +export const RESERVED_SUBDOMAINS = [ + 'about', + 'account', + 'admin', + 'advertising', + 'api', + 'assets', + 'archive', + 'beta', + 'billing', + 'blog', + 'cache', + 'cdn', + 'code', + 'community', + 'dashboard', + 'developer', + 'developers', + 'forum', + 'help', + 'home', + 'http', + 'https', + 'imap', + 'localhost', + 'mail', + 'mobile', + 'news', + 'newsletter', + 'ns1', + 'ns2', + 'ns3', + 'ns4', + 'password', + 'profile', + 'sandbox', + 'script', + 'scripts', + 'setup', + 'signin', + 'signup', + 'smtp', + 'support', + 'status', + 'static', + 'stats', + 'test', + 'update', + 'updates', + 'www', + 'www1', + 'www2', + 'www3', + 'www4', +]; diff --git a/shared/utils/domains.test.js b/shared/utils/domains.test.js new file mode 100644 index 000000000..c0556f47e --- /dev/null +++ b/shared/utils/domains.test.js @@ -0,0 +1,17 @@ +/* eslint-disable flowtype/require-valid-file-annotation */ +import { stripSubdomain } from './domains'; + +describe('#stripSubdomain', () => { + test('to work with localhost', () => { + expect(stripSubdomain('localhost')).toBe('localhost'); + }); + test('to return domains without a subdomain', () => { + expect(stripSubdomain('example')).toBe('example'); + expect(stripSubdomain('example.com')).toBe('example.com'); + expect(stripSubdomain('example.org:3000')).toBe('example.org'); + }); + test('to remove subdomains', () => { + expect(stripSubdomain('test.example.com')).toBe('example.com'); + expect(stripSubdomain('test.example.com:3000')).toBe('example.com'); + }); +}); diff --git a/shared/utils/routeHelpers.js b/shared/utils/routeHelpers.js index 67e6c34ae..a1d761dfd 100644 --- a/shared/utils/routeHelpers.js +++ b/shared/utils/routeHelpers.js @@ -53,22 +53,26 @@ export function mailToUrl(): string { return 'mailto:hello@getoutline.com'; } +export function features(): string { + return `${process.env.URL}/#features`; +} + export function developers(): string { - return '/developers'; + return `${process.env.URL}/developers`; } export function changelog(): string { - return '/changelog'; + return `${process.env.URL}/changelog`; } export function signin(service: string = 'slack'): string { - return `/auth/${service}`; + return `${process.env.URL}/auth/${service}`; } export function about(): string { - return '/about'; + return `${process.env.URL}/about`; } export function privacy(): string { - return '/privacy'; + return `${process.env.URL}/privacy`; } diff --git a/webpack.config.js b/webpack.config.js index cc67d1a7b..761d5cd17 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,7 +16,8 @@ const definePlugin = new webpack.DefinePlugin({ DEPLOYMENT: JSON.stringify(process.env.DEPLOYMENT || 'hosted'), 'process.env': { URL: JSON.stringify(process.env.URL), - SLACK_KEY: JSON.stringify(process.env.SLACK_KEY) + SLACK_KEY: JSON.stringify(process.env.SLACK_KEY), + SUBDOMAINS_ENABLED: JSON.stringify(process.env.SUBDOMAINS_ENABLED) } }); @@ -31,6 +32,9 @@ module.exports = { { test: /\.js$/, loader: 'babel-loader', + exclude: [ + path.join(__dirname, 'node_modules') + ], include: [ path.join(__dirname, 'app'), path.join(__dirname, 'shared'), @@ -54,9 +58,7 @@ module.exports = { }), }, { test: /\.md/, loader: 'raw-loader' }, - ], - // Silence warning https://github.com/localForage/localForage/issues/599 - noParse: [new RegExp('node_modules/localforage/dist/localforage.js')], + ] }, resolve: { modules: [ diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 0723ba771..fa36525e7 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -37,6 +37,7 @@ productionWebpackConfig.plugins = [ URL: JSON.stringify(process.env.URL), NODE_ENV: JSON.stringify('production'), GOOGLE_ANALYTICS_ID: JSON.stringify(process.env.GOOGLE_ANALYTICS_ID), + SUBDOMAINS_ENABLED: JSON.stringify(process.env.SUBDOMAINS_ENABLED) }, }), ]; diff --git a/yarn.lock b/yarn.lock index 85a4f5c87..d3219c706 100644 --- a/yarn.lock +++ b/yarn.lock @@ -87,6 +87,10 @@ version "0.6.6" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" +"@sindresorhus/is@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" + "@tommoor/remove-markdown@0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@tommoor/remove-markdown/-/remove-markdown-0.3.1.tgz#25e7b845d52fcfadf149a3a6a468a931fee7619b" @@ -155,10 +159,6 @@ acorn-jsx@^3.0.0: dependencies: acorn "^3.0.4" -acorn@^1.0.3: - version "1.2.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014" - acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" @@ -417,14 +417,14 @@ assert@^1.1.1: dependencies: util "0.10.3" +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + ast-types-flow@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" -ast-types@0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.15.tgz#8eef0827f04dff0ec8857ba925abe3fea6194e52" - astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -1206,10 +1206,6 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" -base62@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/base62/-/base62-0.1.1.tgz#7b4174c2f94449753b11c2651c083da841a7b084" - base64-js@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" @@ -1399,6 +1395,10 @@ browser-resolve@^1.11.2: dependencies: resolve "1.1.7" +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + browserify-aes@^1.0.0, browserify-aes@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a" @@ -1569,6 +1569,18 @@ cacache@^10.0.4: unique-filename "^1.1.0" y18n "^4.0.0" +cacheable-request@^2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" + dependencies: + clone-response "1.0.2" + get-stream "3.0.0" + http-cache-semantics "3.8.1" + keyv "3.0.0" + lowercase-keys "1.0.0" + normalize-url "2.0.1" + responselike "1.0.2" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -1660,6 +1672,17 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chai@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + pathval "^1.1.0" + type-detect "^4.0.5" + chainsaw@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" @@ -1735,6 +1758,10 @@ charenc@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + cheerio@^0.22.0: version "0.22.0" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" @@ -1892,6 +1919,12 @@ clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" +clone-response@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + dependencies: + mimic-response "^1.0.0" + clone-stats@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" @@ -2004,6 +2037,10 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +commander@2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + commander@2.8.x: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" @@ -2554,15 +2591,15 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64" +debug@3.1.0, debug@^3.0.1, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: ms "2.0.0" -debug@^3.0.1, debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" +debug@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64" dependencies: ms "2.0.0" @@ -2580,12 +2617,22 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" -decompress-response@^3.2.0: +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +decompress-response@^3.2.0, decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" dependencies: mimic-response "^1.0.0" +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + dependencies: + type-detect "^4.0.0" + deep-equal@^1.0.1, deep-equal@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -2696,6 +2743,10 @@ detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" +diff@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" + diff@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -3058,14 +3109,6 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" -es3ify@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/es3ify/-/es3ify-0.1.4.tgz#ad9fa5df1ae34f3f31e1211b5818b2d51078dfd1" - dependencies: - esprima-fb "~3001.0001.0000-dev-harmony-fb" - jstransform "~3.0.0" - through "~2.3.4" - es5-ext@^0.10.12, es5-ext@^0.10.14, es5-ext@^0.10.30, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2: version "0.10.30" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.30.tgz#7141a16836697dbabfaaaeee41495ce29f52c939" @@ -3138,7 +3181,7 @@ escape-html@~1.0.1, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -3295,10 +3338,6 @@ eslint@^4.14.0: table "^4.0.1" text-table "~0.2.0" -esmangle-evaluator@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/esmangle-evaluator/-/esmangle-evaluator-1.0.1.tgz#620d866ef4861b3311f75766d52a8572bb3c6336" - espree@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" @@ -3306,14 +3345,6 @@ espree@^3.5.2: acorn "^5.2.1" acorn-jsx "^3.0.0" -esprima-fb@~15001.1001.0-dev-harmony-fb: - version "15001.1001.0-dev-harmony-fb" - resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz#43beb57ec26e8cf237d3dd8b33e42533577f2659" - -esprima-fb@~3001.0001.0000-dev-harmony-fb, esprima-fb@~3001.1.0-dev-harmony-fb: - version "3001.1.0-dev-harmony-fb" - resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz#b77d37abcd38ea0b77426bb8bc2922ce6b426411" - esprima@^2.6.0: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -3510,15 +3541,6 @@ extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" -falafel@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/falafel/-/falafel-1.2.0.tgz#c18d24ef5091174a497f318cd24b026a25cddab4" - dependencies: - acorn "^1.0.3" - foreach "^2.0.5" - isarray "0.0.1" - object-keys "^1.0.6" - fancy-log@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.0.tgz#45be17d02bb9917d60ccffd4995c999e6c8c9948" @@ -3835,7 +3857,7 @@ fresh@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" -from2@^2.1.0: +from2@^2.1.0, from2@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" dependencies: @@ -3995,11 +4017,15 @@ get-document@1: version "1.0.0" resolved "https://registry.npmjs.org/get-document/-/get-document-1.0.0.tgz#4821bce66f1c24cb0331602be6cb6b12c4f01c4b" +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" -get-stream@^3.0.0: +get-stream@3.0.0, get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -4057,16 +4083,7 @@ glob2base@^0.0.12: dependencies: find-index "^0.1.1" -glob@^4.3.1: - version "4.5.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "^2.0.1" - once "^1.3.0" - -glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: +glob@7.1.2, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -4077,6 +4094,15 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^4.3.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "^2.0.1" + once "^1.3.0" + glob@~3.1.21: version "3.1.21" resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" @@ -4212,6 +4238,28 @@ got@^7.1.0: url-parse-lax "^1.0.0" url-to-options "^1.0.1" +got@^8.0.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" + dependencies: + "@sindresorhus/is" "^0.7.0" + cacheable-request "^2.1.1" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + into-stream "^3.1.0" + is-retry-allowed "^1.1.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + mimic-response "^1.0.0" + p-cancelable "^0.4.0" + p-timeout "^2.0.1" + pify "^3.0.0" + safe-buffer "^5.1.1" + timed-out "^4.0.1" + url-parse-lax "^3.0.0" + url-to-options "^1.0.1" + graceful-fs@^3.0.0: version "3.0.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818" @@ -4230,6 +4278,10 @@ graceful-fs@~1.2.0: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" +growl@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" + growly@^1.2.0, growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -4422,7 +4474,7 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" -he@1.1.x: +he@1.1.1, he@1.1.x: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -4596,6 +4648,10 @@ http-assert@^1.1.0: deep-equal "~1.0.1" http-errors "~1.6.1" +http-cache-semantics@3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" + http-errors@1.4.0, http-errors@^1.2.8, http-errors@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.4.0.tgz#6c0242dea6b3df7afda153c71089b31c6e82aabf" @@ -4792,13 +4848,6 @@ ini@^1.3.4, ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" -inline-process-browser@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/inline-process-browser/-/inline-process-browser-1.0.0.tgz#46a61b153dd3c9b1624b1a00626edb4f7f414f22" - dependencies: - falafel "^1.0.1" - through2 "^0.6.5" - inquirer@^3.0.6: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" @@ -4822,6 +4871,13 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" +into-stream@^3.1.0: + version "3.1.0" + resolved "http://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" + dependencies: + from2 "^2.1.1" + p-is-promise "^1.1.0" + invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" @@ -5100,7 +5156,7 @@ is-resolvable@^1.0.0: dependencies: tryit "^1.0.1" -is-retry-allowed@^1.0.0: +is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" @@ -5647,6 +5703,10 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + json-loader@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" @@ -5731,14 +5791,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jstransform@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-3.0.0.tgz#a2591ab6cee8d97bf3be830dbfa2313b87cd640b" - dependencies: - base62 "0.1.1" - esprima-fb "~3001.1.0-dev-harmony-fb" - source-map "0.1.31" - jsx-ast-utils@^1.4.0: version "1.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" @@ -5784,6 +5836,12 @@ keygrip@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.2.tgz#ad3297c557069dea8bcfe7a4fa491b75c5ddeb91" +keyv@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" + dependencies: + json-buffer "3.0.0" + kind-of@^3.0.2: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -5992,15 +6050,6 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -lie@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.0.2.tgz#ffda21d7bba26f377cad865d3649b2fc8ce39fea" - dependencies: - es3ify "^0.1.3" - immediate "~3.0.5" - inline-process-browser "^1.0.0" - unreachable-branch-transform "^0.3.0" - lie@~3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" @@ -6126,12 +6175,6 @@ loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" -localforage@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.5.0.tgz#6b994e19b56611fa85df3992df397ac4ab66e815" - dependencies: - lie "3.0.2" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -6490,7 +6533,7 @@ lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" -lowercase-keys@^1.0.0: +lowercase-keys@1.0.0, lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" @@ -6780,7 +6823,7 @@ mississippi@^2.0.0: stream-each "^1.1.0" through2 "^2.0.0" -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -6800,6 +6843,21 @@ mobx@^3.1.9: version "3.2.2" resolved "https://registry.yarnpkg.com/mobx/-/mobx-3.2.2.tgz#aa671459bededfd9880c948889a3f62bce09279c" +mocha@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794" + dependencies: + browser-stdout "1.3.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.3" + he "1.1.1" + mkdirp "0.5.1" + supports-color "4.4.0" + moment-timezone@^0.5.0: version "0.5.14" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.14.tgz#4eb38ff9538b80108ba467a458f3ed4268ccfcb1" @@ -7112,6 +7170,14 @@ normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" +normalize-url@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + normalize-url@^1.4.0: version "1.9.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" @@ -7198,7 +7264,7 @@ object-is@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" -object-keys@^1.0.10, object-keys@^1.0.6, object-keys@^1.0.8: +object-keys@^1.0.10, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" @@ -7397,10 +7463,18 @@ p-cancelable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" +p-cancelable@^0.4.0: + version "0.4.1" + resolved "http://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" +p-is-promise@^1.1.0: + version "1.1.0" + resolved "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + p-limit@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" @@ -7421,6 +7495,12 @@ p-timeout@^1.1.1: dependencies: p-finally "^1.0.0" +p-timeout@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" + dependencies: + p-finally "^1.0.0" + package-json@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" @@ -7460,6 +7540,15 @@ parse-asn1@^5.0.0: evp_bytestokey "^1.0.0" pbkdf2 "^3.0.3" +parse-domain@2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/parse-domain/-/parse-domain-2.1.6.tgz#3baac0a1c6b7028dfea0013c99a83a1ecd806ed0" + dependencies: + chai "^4.1.2" + got "^8.0.1" + mkdirp "^0.5.1" + mocha "^4.0.1" + parse-entities@^1.0.2: version "1.1.1" resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-1.1.1.tgz#8112d88471319f27abae4d64964b122fe4e1b890" @@ -7585,6 +7674,10 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" +pathval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -7976,6 +8069,10 @@ prepend-http@^1.0.0, prepend-http@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" @@ -8008,7 +8105,7 @@ prismjs@^1.13.0: optionalDependencies: clipboard "^2.0.0" -private@^0.1.6, private@^0.1.7, private@~0.1.5: +private@^0.1.6, private@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" @@ -8159,6 +8256,14 @@ query-string@^4.1.0, query-string@^4.3.4: object-assign "^4.1.0" strict-uri-encode "^1.0.0" +query-string@^5.0.1: + version "5.1.1" + resolved "http://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -8521,15 +8626,6 @@ realpath-native@^1.0.0: dependencies: util.promisify "^1.0.0" -recast@^0.10.1: - version "0.10.43" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.43.tgz#b95d50f6d60761a5f6252e15d80678168491ce7f" - dependencies: - ast-types "0.8.15" - esprima-fb "~15001.1001.0-dev-harmony-fb" - private "~0.1.5" - source-map "~0.5.0" - rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -8850,6 +8946,12 @@ response-time@~2.3.1: depd "~1.1.0" on-headers "~1.0.1" +responselike@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + dependencies: + lowercase-keys "^1.0.0" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -9390,6 +9492,12 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + dependencies: + is-plain-obj "^1.0.0" + source-list-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" @@ -9416,12 +9524,6 @@ source-map-support@^0.5.0: dependencies: source-map "^0.6.0" -source-map@0.1.31: - version "0.1.31" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.31.tgz#9f704d0d69d9e138a81badf6ebb4fde33d151c61" - dependencies: - amdefine ">=0.0.4" - source-map@0.1.x: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" @@ -9438,7 +9540,7 @@ source-map@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" -source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.6: +source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -9745,6 +9847,12 @@ stylis@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" +supports-color@4.4.0, supports-color@^4.0.0, supports-color@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -9755,12 +9863,6 @@ supports-color@^3.1.2, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^4.0.0, supports-color@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" - dependencies: - has-flag "^2.0.0" - supports-color@^4.2.1: version "4.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" @@ -9889,7 +9991,7 @@ throat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" -through2@^0.6.1, through2@^0.6.2, through2@^0.6.5: +through2@^0.6.1: version "0.6.5" resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" dependencies: @@ -9903,7 +10005,7 @@ through2@^2.0.0, through2@^2.0.1: readable-stream "^2.1.5" xtend "~4.0.1" -through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4: +through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -9925,7 +10027,7 @@ timed-out@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" -timed-out@^4.0.0: +timed-out@^4.0.0, timed-out@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" @@ -10085,6 +10187,10 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + type-is@^1.5.5, type-is@^1.6.14, type-is@~1.6.6: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" @@ -10273,14 +10379,6 @@ unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" -unreachable-branch-transform@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/unreachable-branch-transform/-/unreachable-branch-transform-0.3.0.tgz#d99cc4c6e746d264928845b611db54b0f3474caa" - dependencies: - esmangle-evaluator "^1.0.0" - recast "^0.10.1" - through2 "^0.6.2" - unzipper@^0.8.11: version "0.8.13" resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.8.13.tgz#ea889ca10cdda4dcf604e632f19fc5f81871beae" @@ -10343,6 +10441,12 @@ url-parse-lax@^1.0.0: dependencies: prepend-http "^1.0.1" +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + dependencies: + prepend-http "^2.0.0" + url-to-options@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"