diff --git a/env/dev.js b/env/dev.js index 294a9272..208ee340 100644 --- a/env/dev.js +++ b/env/dev.js @@ -1,13 +1,8 @@ -const infura = { - key: 'zU4GTAQ0LjJNKddbyztc', -}; - const isProduction = false; -const oauthServer = 'https://oauth2-dev.endpass.com:9000/oauth2'; +const oauthServer = 'https://identity-dev.endpass.com/api/v1.1/oauth'; module.exports = { - infura, isProduction, oauthServer, }; diff --git a/env/prod.js b/env/prod.js index 65cd9652..a19b06f2 100644 --- a/env/prod.js +++ b/env/prod.js @@ -1,13 +1,8 @@ -const infura = { - key: 'zU4GTAQ0LjJNKddbyztc', -}; - const isProduction = true; -const oauthServer = 'https://oauth2.endpass.com:9000/oauth2'; +const oauthServer = 'https://identity.endpass.com/api/v1.1/oauth'; module.exports = { - infura, isProduction, oauthServer, }; diff --git a/env/test.js b/env/test.js index 294a9272..208ee340 100644 --- a/env/test.js +++ b/env/test.js @@ -1,13 +1,8 @@ -const infura = { - key: 'zU4GTAQ0LjJNKddbyztc', -}; - const isProduction = false; -const oauthServer = 'https://oauth2-dev.endpass.com:9000/oauth2'; +const oauthServer = 'https://identity-dev.endpass.com/api/v1.1/oauth'; module.exports = { - infura, isProduction, oauthServer, }; diff --git a/package.json b/package.json index b5d2ff9b..356e6f9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@endpass/connect", - "version": "0.22.1-beta", + "version": "0.23.0-beta", "authVersion": "", "main": "./dist/endpass-connect.min.js", "module": "./dist/endpass-connect.esm.js", @@ -25,7 +25,7 @@ "build:lib": "NODE_ENV=production rollup -c", "build:browser": "webpack --mode production", "build:dev": "rollup -c", - "test": "jest && npm run e2e", + "test": "jest", "setup-e2e": "bash e2e-preloader $npm_package_config_repo $npm_package_config_commit", "e2e": "cypress run", "e2e:open": "cypress open", @@ -74,8 +74,8 @@ } }, "dependencies": { - "@endpass/class": "^0.14.4", - "@endpass/utils": "^1.4.2", + "@endpass/class": "^0.14.5", + "@endpass/utils": "^1.7.0", "axios": "^0.19.0", "axios-token-interceptor": "^0.1.0", "lodash.get": "^4.4.2", @@ -94,12 +94,13 @@ "@commitlint/config-conventional": "^8.0.0", "@semantic-release/changelog": "^3.0.2", "@semantic-release/git": "^7.0.8", - "@types/lodash": "^4.14.133", + "@types/lodash": "^4.14.134", "@types/lodash.get": "^4.4.6", - "@types/node": "^12.0.4", + "@types/node": "^12.0.7", "@types/web3": "^1.0.18", - "@typescript-eslint/eslint-plugin": "^1.9.0", - "@typescript-eslint/parser": "^1.9.0", + "@typescript-eslint/eslint-plugin": "^1.10.2", + "@typescript-eslint/parser": "^1.10.2", + "axios-mock-adapter": "^1.16.0", "babel-eslint": "^10.0.1", "babel-loader": "^8.0.6", "commitizen": "^3.1.1", @@ -112,14 +113,14 @@ "eslint-plugin-import": "^2.17.3", "eslint-plugin-prettier": "^3.1.0", "gh-pages": "^2.0.1", - "husky": "^1.3.1", + "husky": "^2.4.0", "jest": "^24.8.0", "jest-localstorage-mock": "^2.4.0", - "lint-staged": "^8.1.7", - "prettier": "^1.17.1", + "lint-staged": "^8.2.0", + "prettier": "^1.18.2", "regenerator-runtime": "^0.13.2", "rimraf": "^2.6.3", - "rollup": "^1.13.1", + "rollup": "^1.15.0", "rollup-plugin-alias": "^1.5.2", "rollup-plugin-babel": "^4.3.2", "rollup-plugin-commonjs": "^10.0.0", @@ -131,7 +132,7 @@ "rollup-plugin-visualizer": "^1.1.1", "semantic-release": "^15.13.12", "typescript": "^3.5.1", - "webpack": "^4.32.2", - "webpack-cli": "^3.3.2" + "webpack": "^4.33.0", + "webpack-cli": "^3.3.4" } } diff --git a/src/Context.js b/src/Context.js index faffa773..46b5ed83 100644 --- a/src/Context.js +++ b/src/Context.js @@ -1,5 +1,6 @@ import Network from '@endpass/class/Network'; import CrossWindowMessenger from '@endpass/class/CrossWindowMessenger'; +import OauthPkceStrategy from '@/class/Oauth/OauthPkceStrategy'; import { Emmiter, InpageProvider, @@ -242,6 +243,7 @@ export default class Context { this.oauthRequestProvider = new Oauth({ ...params, clientId: this.oauthClientId, + strategy: OauthPkceStrategy, }); await this.oauthRequestProvider.init(); } diff --git a/src/class/Bridge.js b/src/class/Bridge.js index e942fe43..e654f299 100644 --- a/src/class/Bridge.js +++ b/src/class/Bridge.js @@ -13,7 +13,7 @@ import Widget from './Widget'; export default class Bridge { /** * @param InstanceType<{import('@Context')} options.context Context link - * @param {String} options.url Url for open dialog + * @param {string} options.url Url for open dialog * @param {any} options.initialPayload initial data for Auth */ constructor({ context, url, initialPayload }) { @@ -124,7 +124,7 @@ export default class Bridge { } /** - * @param {Object} [parameters] + * @param {object} [parameters] * @returns {Element} */ mountWidget(parameters) { @@ -150,7 +150,7 @@ export default class Bridge { * Ask messenger before til it give any answer and resolve promise * Also, it is caches ready state and in the next time just resolve returned * promise - * @returns {Promise} + * @returns {Promise} */ checkReadyState() { /* eslint-disable-next-line */ diff --git a/src/class/Oauth.js b/src/class/Oauth.js deleted file mode 100644 index e8ba8249..00000000 --- a/src/class/Oauth.js +++ /dev/null @@ -1,188 +0,0 @@ -import axios from 'axios'; -import tokenProvider from 'axios-token-interceptor'; -import { isNumeric } from '@endpass/utils/numbers'; -import PopupWindow from '@/class/PopupWindow'; - -export const STORE_KEYS = { - TOKEN: 'token', - EXPIRES: 'expires', - SCOPE: 'scope', -}; - -const { TOKEN, EXPIRES, SCOPE } = STORE_KEYS; - -function getRandomState() { - return ( - Math.random() - .toString(36) - .substring(5) + - Math.random() - .toString(36) - .substring(5) - ); -} - -export default class Oauth { - constructor({ - clientId, - scopes, - popupHeight, - popupWidth, - state, - oauthServer, - }) { - this.clientId = clientId; - this.oauthServer = oauthServer; - this.axiosInstance = null; - this.popupHeight = popupHeight || 1000; - this.popupWidth = popupWidth || 600; - this.state = state || getRandomState(); - - const scopeValue = scopes.join(' '); - - const oldScope = this.getStoredValue(SCOPE); - if (oldScope === scopeValue) { - this.setStoredValue({ - name: TOKEN, - value: this.getStoredValue(TOKEN), - }); - - const expiredValue = this.getStoredValue(EXPIRES) - ? parseInt(this.getStoredValue(EXPIRES), 10) - : null; - this.setStoredValue({ - name: EXPIRES, - value: expiredValue, - }); - } else { - this.setStoredValue({ - name: SCOPE, - value: scopeValue, - }); - } - this.checkTokenValidity(); - } - - async init() { - if (!this.getStoredValue(TOKEN)) { - await this.authorize(); - } - this.createAxiosInstance(); - } - - /** - * Authorizes and returns resulting authorization token - * @private - */ - async authorize() { - const { state } = this; - - const authorizationResult = await PopupWindow.open( - { - client_id: this.clientId, - scope: this.getStoredValue(SCOPE), - state, - response_type: 'token', - }, - { - oauthServer: this.oauthServer, - height: this.popupHeight, - width: this.popupWidth, - }, - ); - if (authorizationResult.state !== state) { - throw new Error('Authorization failed: state check unsuccessful'); - } - this.setStoredValue({ - name: EXPIRES, - value: new Date().getTime() + authorizationResult.expires_in * 1000, - }); - - this.setStoredValue({ - name: TOKEN, - value: authorizationResult.access_token, - }); - } - - logout() { - this.clearStoredValue(SCOPE); - this.clearStoredValue(TOKEN); - this.clearStoredValue(EXPIRES); - } - - setStoredValue({ name, value }) { - localStorage.setItem(`${this.clientId}${name}`, value); - } - - getStoredValue(name) { - return localStorage.getItem(`${this.clientId}${name}`); - } - - clearStoredValue(name) { - localStorage.removeItem(`${this.clientId}${name}`); - } - - /** - * Returns saved authorization token or calls for freash authorization - * @private - * @returns {String} authVersion token - */ - async getToken() { - this.checkTokenValidity(); - if (!this.getStoredValue(TOKEN)) { - await this.authorize(); - } - return this.getStoredValue(TOKEN); - } - - /** - * Returns axios instance with token providing interceptor - * @returns {Axios} axios instance - */ - createAxiosInstance() { - this.axiosInstance = axios.create(); - this.axiosInstance.interceptors.request.use( - tokenProvider({ - getToken: () => this.getToken(), - }), - ); - } - - /** - * Sets oauth popup parameters - * @param {Object} params Parameters object - * @param {Number} [width] Oauth popup width - * @param {Number} [height] Oauth popup height - */ - setPopupParams({ height, width }) { - this.popupWidth = isNumeric(width) ? width : this.popupWidth; - this.popupHeight = isNumeric(height) ? height : this.popupHeight; - } - - checkTokenValidity() { - const expire = this.getStoredValue(EXPIRES) - 0; - if (!isNumeric(expire) || new Date().getTime() >= expire) { - this.clearStoredValue(TOKEN); - this.clearStoredValue(EXPIRES); - } - } - - /** - * Makes api request with authorization token - * @param {Object} [options] Request parameters object - * @param {String} url Request url - * @param {String} method Request http method - * @param {Object} [params] - Request parameters - * @param {Object} [headers] - Request headers - * @param {Object|string} [data] - Request body - * @returns {Promise} Request promise - */ - async request(options) { - if (!this.axiosInstance) { - await this.createAxiosInstance(); - } - return this.axiosInstance({ - ...options, - }); - } -} diff --git a/src/class/Oauth/Oauth.js b/src/class/Oauth/Oauth.js new file mode 100644 index 00000000..4136dfa1 --- /dev/null +++ b/src/class/Oauth/Oauth.js @@ -0,0 +1,144 @@ +// @ts-check +import axios from 'axios'; + +// @ts-ignore +import tokenProvider from 'axios-token-interceptor'; +// @ts-ignore +import { isNumeric } from '@endpass/utils/numbers'; +// @ts-ignore +import LocalStorage from '@endpass/class/LocalStorage'; + +/** @typedef {string} Token */ + +export default class Oauth { + /** + * + * @param {object} params Params for constructor + * @param {string} params.clientId Client id for oauth server + * @param {string[]} params.scopes Scopes list + * @param {OauthStrategy} params.strategy Strategy for get TokenObject + * @param {number=} [params.popupHeight] Popup window height + * @param {number=} [params.popupWidth] Popup window width + * @param {string=} [params.oauthServer] Url for oauth server + */ + constructor({ + clientId, + scopes, + popupHeight, + popupWidth, + oauthServer, + strategy, + }) { + this.clientId = clientId; + this.oauthServer = oauthServer || ENV.oauthServer; + /** @type {number} */ + this.popupHeight = popupHeight || 1000; + /** @type {number} */ + this.popupWidth = popupWidth || 600; + this.axiosInstance = this.createAxiosInstance(); + this.strategy = strategy; + + this.scopesString = scopes.sort().join(' '); + const storedData = this.getTokenObjectFromStore(); + + if (storedData && storedData.scope !== this.scopesString) { + LocalStorage.remove(this.clientId); + } + } + + /** + * Initiate token + * @return {Promise} + */ + async init() { + const token = await this.getToken(); + return token; + } + + /** + * Authorizes and update token object + * @private + * @return {Promise} + */ + async updateTokenObject() { + const tokenObject = await this.strategy.getTokenObject( + this.oauthServer, + { + client_id: this.clientId, + scope: this.scopesString, + }, + { + height: this.popupHeight, + width: this.popupWidth, + }, + ); + + LocalStorage.save(this.clientId, tokenObject); + return tokenObject; + } + + logout() { + LocalStorage.remove(this.clientId); + } + + /** + * Return stored token object + * @private + * @return {TokenObject | null} + */ + getTokenObjectFromStore() { + return LocalStorage.load(this.clientId); + } + + /** + * Returns saved authorization token or calls for fresh authorization + * @private + * @return {Promise} authVersion token + */ + async getToken() { + let tokenObject = this.getTokenObjectFromStore(); + + if (tokenObject === null || new Date().getTime() >= tokenObject.expires) { + LocalStorage.remove(this.clientId); + tokenObject = await this.updateTokenObject(); + } + + return tokenObject.token; + } + + /** + * Returns axios instance with token providing interceptor + * @private + * @returns {import('axios').AxiosInstance} instance instance + */ + createAxiosInstance() { + const instance = axios.create(); + instance.interceptors.request.use( + tokenProvider({ + getToken: () => this.getToken(), + }), + ); + return instance; + } + + /** + * Sets oauth popup parameters + * @param {object} params Parameters object + * @param {number=} [params.width] Oauth popup width + * @param {number=} [params.height] Oauth popup height + */ + setPopupParams({ height = this.popupHeight, width = this.popupWidth }) { + this.popupWidth = isNumeric(width) ? width : this.popupWidth; + this.popupHeight = isNumeric(height) ? height : this.popupHeight; + } + + /** + * Makes api request with authorization token + * @param {import('axios').AxiosRequestConfig} [options] Request parameters object + */ + request(options) { + return this.axiosInstance({ + ...options, + }); + } +} diff --git a/src/class/Oauth/OauthPkceStrategy.js b/src/class/Oauth/OauthPkceStrategy.js new file mode 100644 index 00000000..9781d01a --- /dev/null +++ b/src/class/Oauth/OauthPkceStrategy.js @@ -0,0 +1,80 @@ +// @ts-check + +import axios from 'axios'; +import PopupWindow from '@/class/PopupWindow'; +import pkce from '@/class/Oauth/pkce'; + +/** @type {OauthStrategy} */ +export default class OauthPkceStrategy { + /** + * + * @private + * @param {string} oauthServer + * @param {object} fields + * @return {Promise} + */ + static async exchangeCodeToToken(oauthServer, fields) { + const formData = Object.keys(fields).reduce((form, key) => { + form.append(key, fields[key]); + return form; + }, new FormData()); + + const url = `${oauthServer}/token`; + + const { data } = await axios.post(url, formData); + return data; + } + + /** + * @param {string} oauthServer server url + * @param {object} params params for oauth authorize + * @param {string} params.client_id client id for oauth server + * @param {string} params.scope scope for oauth + * @param {object=} [options] options for popup + * @return {Promise} + */ + static async getTokenObject(oauthServer, params, options) { + // Create and store a random "state" value + const state = pkce.generateRandomString(); + // Create and store a new PKCE code_verifier (the plaintext random secret) + const codeVerifier = pkce.generateRandomString(); + // Hash and base64-urlencode the secret to use as the challenge + const codeChallenge = await pkce.challengeFromVerifier(codeVerifier); + + const popupResult = await PopupWindow.open( + oauthServer, + { + ...params, + state, + response_type: 'code', + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }, + options, + ); + + if (popupResult.state !== state) { + throw new Error('Authorization failed: state check unsuccessful'); + } + + if (popupResult.error) { + throw new Error(`Authorization failed: ${popupResult.error}`); + } + + const tokenResult = await OauthPkceStrategy.exchangeCodeToToken( + oauthServer, + { + grant_type: 'authorization_code', + code: popupResult.code, + client_id: params.client_id, + code_verifier: codeVerifier, + }, + ); + + return { + token: tokenResult.access_token, + expires: new Date().getTime() + tokenResult.expires_in * 1000, + scope: params.scope, + }; + } +} diff --git a/src/class/Oauth/index.js b/src/class/Oauth/index.js new file mode 100644 index 00000000..67619025 --- /dev/null +++ b/src/class/Oauth/index.js @@ -0,0 +1,2 @@ +export { default } from './Oauth'; +export { default as OauthPkceStrategy } from './OauthPkceStrategy'; diff --git a/src/class/Oauth/pkce.js b/src/class/Oauth/pkce.js new file mode 100644 index 00000000..d78c707b --- /dev/null +++ b/src/class/Oauth/pkce.js @@ -0,0 +1,57 @@ +// @ts-check +// PKCE HELPER FUNCTIONS + +/** + * Generate a secure random string using the browser crypto functions + * @return {string} + */ +function generateRandomString() { + const array = new Uint32Array(28); + window.crypto.getRandomValues(array); + return Array.from(array, dec => `0${dec.toString(16)}`.substr(-2)).join(''); +} + +/** + * Calculate the SHA256 hash of the input text. + * Returns a promise that resolves to an ArrayBuffer + * @param {string} plain + * @return {PromiseLike} + */ +function sha256(plain) { + const encoder = new TextEncoder(); + const data = encoder.encode(plain); + return window.crypto.subtle.digest('SHA-256', data); +} + +/** + * Base64-urlencodes the input + * @param {ArrayBuffer} buffer + * @return {string} + */ +function base64urlencode(buffer) { + // Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts. + // btoa accepts chars only within ascii 0-255 and base64 encodes them. + // Then convert the base64 encoded to base64url encoded + // (replace + with -, replace / with _, trim trailing =) + + // @ts-ignore + return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); +} + +/** + * Return the base64-urlencoded sha256 hash for the PKCE challenge + * @param {string} v + * @return {Promise} + */ +async function challengeFromVerifier(v) { + const hashed = await sha256(v); + return base64urlencode(hashed); +} + +export default { + challengeFromVerifier, + generateRandomString, +}; diff --git a/src/class/PopupWindow.js b/src/class/PopupWindow.js index d8a7a713..1ac6eb2c 100644 --- a/src/class/PopupWindow.js +++ b/src/class/PopupWindow.js @@ -1,27 +1,15 @@ import mapToQueryString from '@endpass/utils/mapToQueryString'; - -function hashToMap(path) { - const lines = path.replace(/^\#\/?/, '').split('&'); - const query = lines.reduce((map, line) => { - const values = line.split('='); - const key = values[0]; - if (key) { - // eslint-disable-next-line - map[key] = values[1]; - } - return map; - }, {}); - return query; -} +import queryStringToMap from '@endpass/utils/queryStringToMap'; export default class PopupWindow { - constructor(params, windowOptions = {}) { + constructor(oauthServer, params, windowOptions = {}) { this.windowOptions = { height: windowOptions.height || 1000, width: windowOptions.width || 600, }; this.id = 'endpass-oauth-authorize'; - const server = windowOptions.oauthServer || ENV.oauthServer; + + const server = oauthServer || ENV.oauthServer; this.url = mapToQueryString(`${server}/auth`, params); } @@ -55,7 +43,12 @@ export default class PopupWindow { return; } - const params = hashToMap(popup.location.hash); + const query = (popup.location.hash || popup.location.search).replace( + /^\#\/?/, + '', + ); + + const params = queryStringToMap(query); resolve(params); this.close(); @@ -64,10 +57,6 @@ export default class PopupWindow { }); } - then(...args) { - return this.promise.then(...args); - } - cancel() { if (this.intervalId) { window.clearInterval(this.intervalId); @@ -86,6 +75,6 @@ export default class PopupWindow { popup.openPopup(); popup.poll(); - return popup; + return popup.promise; } } diff --git a/tests/unit/setup.js b/tests/unit/setup.js index 30df8288..688fc561 100644 --- a/tests/unit/setup.js +++ b/tests/unit/setup.js @@ -1,7 +1,4 @@ // import '../../src/util/__mocks__/message.mock'; import 'jest-localstorage-mock'; - -global.flushPromises = () => new Promise(resolve => setImmediate(resolve)); - -global.open = function() {}; -global.close = function() {}; +import './setup/flushPromises'; +import './setup/openClose'; diff --git a/tests/unit/setup/flushPromises.js b/tests/unit/setup/flushPromises.js new file mode 100644 index 00000000..e0c495f0 --- /dev/null +++ b/tests/unit/setup/flushPromises.js @@ -0,0 +1 @@ +global.flushPromises = () => new Promise(resolve => setImmediate(resolve)); diff --git a/tests/unit/setup/openClose.js b/tests/unit/setup/openClose.js new file mode 100644 index 00000000..650ee588 --- /dev/null +++ b/tests/unit/setup/openClose.js @@ -0,0 +1,2 @@ +global.open = function() {}; +global.close = function() {}; diff --git a/tests/unit/spec/class/Oauth.spec.js b/tests/unit/spec/class/Oauth.spec.js deleted file mode 100644 index ba3e78d2..00000000 --- a/tests/unit/spec/class/Oauth.spec.js +++ /dev/null @@ -1,196 +0,0 @@ -import Oauth, { STORE_KEYS } from '@/class/Oauth'; -import PopupWindow from '@/class/PopupWindow'; - -jest.mock('@/class/PopupWindow', () => { - return { - open: jest.fn(), - }; -}); - -describe('Oauth class', () => { - let oauth; - const scopes = ['chpok']; - const clientId = 'kek'; - const token = 'bam'; - - const response = { - client_id: clientId, - scope: scopes.join(','), - response_type: 'token', - }; - - function popupOpenImplement(options = {}) { - PopupWindow.open.mockImplementation(params => { - return { - state: params.state, - expires_in: 3600, - access_token: token, - ...options, - }; - }); - } - beforeEach(() => { - jest.clearAllMocks(); - oauth = new Oauth({ - scopes, - clientId, - }); - }); - - describe('constructor', () => { - it('should use existing token if not expired', async () => { - expect.assertions(4); - - // prepare old token - const oldToken = 'oldToken'; - popupOpenImplement({ - access_token: oldToken, - }); - - await oauth.authorize(); - - expect(oauth.getStoredValue(STORE_KEYS.TOKEN)).toBe(oldToken); - - const newToken = await oauth.getToken(); - - expect(newToken).toBe(oldToken); - - // next get token - oauth = new Oauth({ - scopes, - clientId, - }); - - const secondToken = await oauth.getToken(); - - expect(PopupWindow.open).toBeCalledTimes(1); - expect(secondToken).toBe(oldToken); - }); - - it('should get new token if old is expired', async () => { - expect.assertions(3); - - popupOpenImplement({ - expires_in: -3600, - }); - - await oauth.authorize(); - - expect(oauth.getStoredValue(STORE_KEYS.TOKEN)).toBe(token); - - const newToken = 'newToken'; - popupOpenImplement({ - access_token: newToken, - }); - - const secondToken = await oauth.getToken(); - - expect(secondToken).toBe(newToken); - expect(PopupWindow.open).toBeCalledTimes(2); - }); - }); - - describe('logout', () => { - it('should clear scope, token, expires in instance and local storage', async () => { - expect.assertions(3); - - popupOpenImplement(); - await oauth.authorize(); - - oauth.logout(); - - expect(oauth.getStoredValue(STORE_KEYS.SCOPE)).toBe(null); - expect(oauth.getStoredValue(STORE_KEYS.TOKEN)).toBe(null); - expect(oauth.getStoredValue(STORE_KEYS.EXPIRES)).toBe(null); - }); - }); - - describe('getToken', () => { - beforeEach(() => { - oauth.authorize = jest.fn(); - }); - - it('should try to authorize if token is not present', async () => { - expect.assertions(1); - - await oauth.getToken(); - - expect(oauth.authorize).toHaveBeenCalled(); - }); - }); - - describe('authorize', () => { - it('should call method with correct params and set token', async () => { - expect.assertions(3); - - const checkToken = 'checkToken'; - popupOpenImplement({ - access_token: checkToken, - }); - - await oauth.authorize(); - - const newToken = await oauth.getToken(); - - expect(newToken).toBe(checkToken); - expect(PopupWindow.open).toHaveBeenCalledWith( - expect.objectContaining(response), - { height: 1000, width: 600 }, - ); - expect(PopupWindow.open).toBeCalledTimes(1); - }); - - it('should set token, and expire time and save it to local storage', async () => { - expect.assertions(4); - - PopupWindow.open.mockImplementation(params => { - return { - state: params.state, - expires_in: 3600, - access_token: token, - }; - }); - await oauth.authorize(); - - const expiresValue = oauth.getStoredValue(STORE_KEYS.EXPIRES); - - expect(oauth.getStoredValue(STORE_KEYS.TOKEN)).toBe(token); - expect(expiresValue).toMatch(/[0-9]{13}/); - expect(expiresValue - 0).toBeGreaterThanOrEqual(new Date().getTime()); - expect(expiresValue - 0).toBeLessThanOrEqual( - new Date().getTime() + 3600 * 1000, - ); - }); - }); - - describe('setPopupParams', () => { - it('should use default params', async () => { - expect.assertions(1); - - popupOpenImplement(); - await oauth.authorize(); - - expect(PopupWindow.open).toHaveBeenCalledWith( - expect.objectContaining(response), - { height: 1000, width: 600 }, - ); - }); - - it('should set popup params', async () => { - expect.assertions(1); - - oauth.setPopupParams({ - width: 'abc', - height: 10, - }); - - popupOpenImplement(); - await oauth.authorize(); - - expect(PopupWindow.open).toHaveBeenCalledWith( - expect.objectContaining(response), - { height: 10, width: 600 }, - ); - }); - }); -}); diff --git a/tests/unit/spec/class/Oauth/Oauth.spec.js b/tests/unit/spec/class/Oauth/Oauth.spec.js new file mode 100644 index 00000000..81911572 --- /dev/null +++ b/tests/unit/spec/class/Oauth/Oauth.spec.js @@ -0,0 +1,258 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import Oauth, { OauthPkceStrategy } from '@/class/Oauth'; +import PopupWindow from '@/class/PopupWindow'; + +jest.mock('@/class/PopupWindow', () => { + return { + open: jest.fn(), + }; +}); + +jest.mock('@/class/Oauth/pkce', () => { + return { + generateRandomString: jest.fn().mockReturnValue('pkce-random-string'), + challengeFromVerifier: jest + .fn() + .mockImplementation(random => `verifier -> ${random}`), + }; +}); + +describe('Oauth class', () => { + let axiosMock; + + let oauth; + const scopes = ['chpok']; + const clientId = 'kek'; + const token = 'bam'; + const oauthServer = 'https://identity-dev.endpass.com/api/v1.1/oauth'; + + const response = { + client_id: 'kek', + code_challenge: 'verifier -> pkce-random-string', + code_challenge_method: 'S256', + response_type: 'code', + scope: scopes.join(' '), + state: 'pkce-random-string', + }; + + PopupWindow.open.mockImplementation((serverUrl, params) => { + return { + state: params.state, + expires_in: 3600, + access_token: token, + ...params, + }; + }); + + function mockOauthTokenResult(result = {}, status = 200) { + axiosMock.onPost(`${oauthServer}/token`).reply(() => { + return [status, result]; + }); + } + + beforeEach(() => { + jest.clearAllMocks(); + axiosMock = new MockAdapter(axios); + oauth = new Oauth({ + scopes, + clientId, + strategy: OauthPkceStrategy, + }); + }); + + describe('constructor', () => { + it('should use existing token if not expired', async () => { + expect.assertions(4); + + // prepare old token + const oldToken = 'oldToken'; + mockOauthTokenResult({ + expires_in: 3600, + access_token: oldToken, + }); + + await oauth.updateTokenObject(); + + expect(oauth.getTokenObjectFromStore().token).toBe(oldToken); + + const newToken = await oauth.getToken(); + + expect(newToken).toBe(oldToken); + + // next get token + oauth = new Oauth({ + scopes, + clientId, + strategy: OauthPkceStrategy, + }); + + const secondToken = await oauth.getToken(); + + expect(PopupWindow.open).toBeCalledTimes(1); + expect(secondToken).toBe(oldToken); + }); + + it('should get new token if old is expired', async () => { + expect.assertions(3); + + mockOauthTokenResult({ + expires_in: -3600, + access_token: token, + }); + + await oauth.updateTokenObject(); + + expect(oauth.getTokenObjectFromStore().token).toBe(token); + + const newToken = 'newToken'; + mockOauthTokenResult({ + access_token: newToken, + }); + + const secondToken = await oauth.getToken(); + + expect(secondToken).toBe(newToken); + expect(PopupWindow.open).toBeCalledTimes(2); + }); + }); + + describe('logout', () => { + it('should clear scope, token, expires in instance and local storage', async () => { + expect.assertions(4); + + mockOauthTokenResult({ + access_token: token, + expires_in: 3600, + }); + await oauth.updateTokenObject(); + + expect(oauth.getTokenObjectFromStore().scope).toBe('chpok'); + expect(oauth.getTokenObjectFromStore().token).toBe(token); + expect(oauth.getTokenObjectFromStore().expires).toBeGreaterThanOrEqual( + new Date().getTime(), + ); + + oauth.logout(); + + expect(oauth.getTokenObjectFromStore()).toBe(null); + }); + }); + + describe('getToken', () => { + beforeEach(() => { + oauth.updateTokenObject = jest.fn().mockResolvedValue(token); + }); + + it('should try to update tokenObject if token is not present', async () => { + expect.assertions(1); + + await oauth.getToken(); + + expect(oauth.updateTokenObject).toHaveBeenCalled(); + }); + }); + + describe('updateTokenObject', () => { + it('should call method with correct params and set token', async () => { + expect.assertions(3); + + const checkToken = 'checkToken'; + mockOauthTokenResult({ + access_token: checkToken, + expires_in: 3600, + }); + + await oauth.updateTokenObject(); + + const newToken = await oauth.getToken(); + + expect(newToken).toBe(checkToken); + expect(PopupWindow.open).toHaveBeenCalledWith(oauthServer, response, { + height: 1000, + width: 600, + }); + expect(PopupWindow.open).toBeCalledTimes(1); + }); + + it('should set token, and expire time and save it to local storage', async () => { + expect.assertions(4); + + mockOauthTokenResult({ + expires_in: 3600, + access_token: token, + }); + await oauth.updateTokenObject(); + + const expiresValue = oauth.getTokenObjectFromStore().expires; + + expect(oauth.getTokenObjectFromStore().token).toBe(token); + expect(expiresValue.toString()).toMatch(/[0-9]{13}/); + expect(expiresValue - 0).toBeGreaterThanOrEqual(new Date().getTime()); + expect(expiresValue - 0).toBeLessThanOrEqual( + new Date().getTime() + 3600 * 1000, + ); + }); + + it('should drop token, if update error', async () => { + expect.assertions(3); + + mockOauthTokenResult({ + expires_in: -3600, + access_token: token, + }); + await oauth.updateTokenObject(); + + expect(oauth.getTokenObjectFromStore()).not.toBe(null); + expect(oauth.getTokenObjectFromStore().token).toBe(token); + + mockOauthTokenResult({}, 404); + try { + await oauth.getToken(); + } catch (e) {} + + expect(oauth.getTokenObjectFromStore()).toBe(null); + }); + }); + + describe('setPopupParams', () => { + it('should use default params', async () => { + expect.assertions(1); + + mockOauthTokenResult({ + expires_in: 3600, + access_token: token, + }); + + await oauth.updateTokenObject(); + + expect(PopupWindow.open).toHaveBeenCalledWith( + oauthServer, + expect.objectContaining(response), + { height: 1000, width: 600 }, + ); + }); + + it('should set popup params', async () => { + expect.assertions(1); + + mockOauthTokenResult({ + expires_in: 3600, + access_token: token, + }); + + oauth.setPopupParams({ + width: 'abc', + height: 10, + }); + + await oauth.updateTokenObject(); + + expect(PopupWindow.open).toHaveBeenCalledWith( + oauthServer, + expect.objectContaining(response), + { height: 10, width: 600 }, + ); + }); + }); +}); diff --git a/types/OauthStrategy.d.ts b/types/OauthStrategy.d.ts new file mode 100644 index 00000000..3ec1128c --- /dev/null +++ b/types/OauthStrategy.d.ts @@ -0,0 +1,9 @@ +type StrategyParams = { + client_id: string, + scope: string, +}; + +declare type OauthStrategy = { + getTokenObject: (oauthUrl: string, params: StrategyParams, popupOptions: object) => TokenObject, + [key: string]: any, +}; diff --git a/types/TokenObject.d.ts b/types/TokenObject.d.ts new file mode 100644 index 00000000..0858f7cc --- /dev/null +++ b/types/TokenObject.d.ts @@ -0,0 +1,5 @@ +declare type TokenObject = { + token: string, + expires: number, + scope: string, +}; diff --git a/types/globals.d.ts b/types/globals.d.ts index 7151e0f2..ab37d6b2 100644 --- a/types/globals.d.ts +++ b/types/globals.d.ts @@ -1,2 +1,6 @@ +declare namespace ENV { + const oauthServer: string; + const isProduction: boolean; +} declare type Listener = (...args: any) => void diff --git a/yarn.lock b/yarn.lock index 43a95c13..17c5e382 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@endpass/class@^0.14.4": - version "0.14.4" - resolved "https://registry.yarnpkg.com/@endpass/class/-/class-0.14.4.tgz#9b82f4bd838e9fe75c19e42ad5a846d3caea0079" - integrity sha512-cjtzFjjljSE/rhQ53EOi1TEUKggc8nM3bHThHchqNxIkA4k7jt/CnE+s5SWsBjbwrpGuiPs0GuknCcKVXFSBpA== +"@endpass/class@^0.14.5": + version "0.14.5" + resolved "https://registry.yarnpkg.com/@endpass/class/-/class-0.14.5.tgz#66b4940c8f5a9bb9063c5e51b69f32a4fdf9e6e1" + integrity sha512-Yfr+3PMeUXiW1OYSYHalxvx5mZK0gYtsq2BzfnHnxew17FdOROmZHCVbdq/Ihkrf08XjxmjBv2Jz7BQZeCD7ww== dependencies: "@endpass/utils" "^1.4.2" "@ledgerhq/hw-app-eth" "^4.24.0" @@ -1012,6 +1012,22 @@ node-notifier "^5.4.0" secp256k1 "^3.7.0" +"@endpass/utils@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@endpass/utils/-/utils-1.7.0.tgz#64742812109494eb5e8d51e4ab3d9b1028773be2" + integrity sha512-iO7mqo4UDNHM3nfUz5W25U10eRjzii67Ud3tplLInCJk0Gkn2Yb6L6n9zAY+YplzPBp+YQIbDUAP0hFsg5EvDw== + dependencies: + bip39 "^3.0.2" + bs58check "^2.1.2" + dayjs "^1.8.14" + eth-ecies "^1.0.3" + ethereumjs-wallet "^0.6.3" + export-files "^2.1.1" + keythereum "^1.0.4" + lodash "^4.17.11" + node-notifier "^5.4.0" + secp256k1 "^3.7.0" + "@jest/console@^24.7.1": version "24.7.1" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" @@ -1398,6 +1414,11 @@ dependencies: "@types/node" "*" +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -1435,10 +1456,10 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.122.tgz#3e31394c38cf1e5949fb54c1192cbc406f152c6c" integrity sha512-9IdED8wU93ty8gP06ninox+42SBSJHp2IAamsSYMUY76mshRTeUsid/gtbl8ovnOwy8im41ib4cxTiIYMXGKew== -"@types/lodash@^4.14.133": - version "4.14.133" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.133.tgz#430721c96da22dd1694443e68e6cec7ba1c1003d" - integrity sha512-/3JqnvPnY58GLzG3Y7fpphOhATV1DDZ/Ak3DQufjlRK5E4u+s0CfClfNFtAGBabw+jDGtRFbOZe+Z02ZMWCBNQ== +"@types/lodash@^4.14.134": + version "4.14.134" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.134.tgz#9032b440122db3a2a56200e91191996161dde5b9" + integrity sha512-2/O0khFUCFeDlbi7sZ7ZFRCcT812fAeOLm7Ev4KbwASkZ575TDrDcY7YyaoHdTOzKcNbfiwLYZqPmoC4wadrsw== "@types/node@*": version "11.11.0" @@ -1450,10 +1471,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a" integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ== -"@types/node@^12.0.3", "@types/node@^12.0.4": - version "12.0.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.4.tgz#46832183115c904410c275e34cf9403992999c32" - integrity sha512-j8YL2C0fXq7IONwl/Ud5Kt0PeXw22zGERt+HSSnwbKOJVsAGkEz3sFCYwaF9IOuoG1HOtE0vKCj6sXF7Q0+Vaw== +"@types/node@^12.0.7": + version "12.0.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.7.tgz#4f2563bad652b2acb1722d7e7aae2b0ff62d192c" + integrity sha512-1YKeT4JitGgE4SOzyB9eMwO0nGVNkNEsm9qlIt1Lqm/tG2QEiSMTD4kS3aO6L+w5SClLVxALmIBESK6Mk5wX0A== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1490,40 +1511,39 @@ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0" integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA== -"@typescript-eslint/eslint-plugin@^1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.9.0.tgz#29d73006811bf2563b88891ceeff1c5ea9c8d9c6" - integrity sha512-FOgfBorxjlBGpDIw+0LaZIXRX6GEEUfzj8LXwaQIUCp+gDOvkI+1WgugJ7SmWiISqK9Vj5r8S7NDKO/LB+6X9A== +"@typescript-eslint/eslint-plugin@^1.10.2": + version "1.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.10.2.tgz#552fc64cfcb19c6162190360217c945e8faa330a" + integrity sha512-7449RhjE1oLFIy5E/5rT4wG5+KsfPzakJuhvpzXJ3C46lq7xywY0/Rjo9ZBcwrfbk0nRZ5xmUHkk7DZ67tSBKw== dependencies: - "@typescript-eslint/experimental-utils" "1.9.0" - "@typescript-eslint/parser" "1.9.0" + "@typescript-eslint/experimental-utils" "1.10.2" eslint-utils "^1.3.1" functional-red-black-tree "^1.0.1" regexpp "^2.0.1" - requireindex "^1.2.0" tsutils "^3.7.0" -"@typescript-eslint/experimental-utils@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-1.9.0.tgz#a92777d0c92d7bc8627abd7cdb06cdbcaf2b39e8" - integrity sha512-1s2dY9XxBwtS9IlSnRIlzqILPyeMly5tz1bfAmQ84Ul687xBBve5YsH5A5EKeIcGurYYqY2w6RkHETXIwnwV0A== +"@typescript-eslint/experimental-utils@1.10.2": + version "1.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-1.10.2.tgz#cd548c03fc1a2b3ba5c136d1599001a1ede24215" + integrity sha512-Hf5lYcrnTH5Oc67SRrQUA7KuHErMvCf5RlZsyxXPIT6AXa8fKTyfFO6vaEnUmlz48RpbxO4f0fY3QtWkuHZNjg== dependencies: - "@typescript-eslint/typescript-estree" "1.9.0" + "@typescript-eslint/typescript-estree" "1.10.2" + eslint-scope "^4.0.0" -"@typescript-eslint/parser@1.9.0", "@typescript-eslint/parser@^1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.9.0.tgz#5796cbfcb9a3a5757aeb671c1ac88d7a94a95962" - integrity sha512-CWgC1XrQ34H/+LwAU7vY5xteZDkNqeAkeidEpJnJgkKu0yqQ3ZhQ7S+dI6MX4vmmM1TKRbOrKuXc6W0fIHhdbA== +"@typescript-eslint/parser@^1.10.2": + version "1.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.10.2.tgz#36cfe8c6bf1b6c1dd81da56f88c8588f4b1a852b" + integrity sha512-xWDWPfZfV0ENU17ermIUVEVSseBBJxKfqBcRCMZ8nAjJbfA5R7NWMZmFFHYnars5MjK4fPjhu4gwQv526oZIPQ== dependencies: - "@typescript-eslint/experimental-utils" "1.9.0" - "@typescript-eslint/typescript-estree" "1.9.0" - eslint-scope "^4.0.0" + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "1.10.2" + "@typescript-eslint/typescript-estree" "1.10.2" eslint-visitor-keys "^1.0.0" -"@typescript-eslint/typescript-estree@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.9.0.tgz#5d6d49be936e96fb0f859673480f89b070a5dd9b" - integrity sha512-7Eg0TEQpCkTsEwsl1lIzd6i7L3pJLQFWesV08dS87bNz0NeSjbL78gNAP1xCKaCejkds4PhpLnZkaAjx9SU8OA== +"@typescript-eslint/typescript-estree@1.10.2": + version "1.10.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.10.2.tgz#8403585dd74b6cfb6f78aa98b6958de158b5897b" + integrity sha512-Kutjz0i69qraOsWeI8ETqYJ07tRLvD9URmdrMoF10bG8y8ucLmPtSxROvVejWvlJUGl2et/plnMiKRDW+rhEhw== dependencies: lodash.unescape "4.0.1" semver "5.5.0" @@ -2114,6 +2134,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axios-mock-adapter@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.16.0.tgz#cdd55bb60d8cb3fcd77fdb9cbb269e47b8b95180" + integrity sha512-m2D8ngMTQ5p4zZNBsPKoENgwz5rDfd0pZmXI/spdE2eeeKIcR3jquk+NRiBVFtb9UJlciBYplNzSUmgQ6X385Q== + dependencies: + deep-equal "^1.0.1" + axios-token-interceptor@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/axios-token-interceptor/-/axios-token-interceptor-0.1.0.tgz#e848bbc57fa694ddb83e4ad0982db3304b88a926" @@ -3329,7 +3356,7 @@ cors@^2.8.1: object-assign "^4" vary "^1" -cosmiconfig@^5.0.1, cosmiconfig@^5.0.7: +cosmiconfig@^5.0.1: version "5.0.7" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04" integrity sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA== @@ -3660,6 +3687,11 @@ dedent@0.7.0, dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -4813,11 +4845,6 @@ find-npm-prefix@^1.0.2: resolved "https://registry.yarnpkg.com/find-npm-prefix/-/find-npm-prefix-1.0.2.tgz#8d8ce2c78b3b4b9e66c8acc6a37c231eb841cfdf" integrity sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA== -find-parent-dir@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" - integrity sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ= - find-root@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -4837,6 +4864,13 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.0.0.tgz#c367f8024de92efb75f2d4906536d24682065c3a" + integrity sha512-zoH7ZWPkRdgwYCDVoQTzqjG8JSPANhtvLhh4KVUHyKnaUJJrNeFmWIkTcNuJmR3GLMEmGYEf2S2bjgx26JTF+Q== + dependencies: + locate-path "^5.0.0" + find-versions@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.0.0.tgz#2c05a86e839c249101910100b354196785a2c065" @@ -5122,7 +5156,7 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" integrity sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg== -get-stdin@7.0.0: +get-stdin@7.0.0, get-stdin@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ== @@ -5606,21 +5640,21 @@ humanize-url@^1.0.0: normalize-url "^1.0.0" strip-url-auth "^1.0.0" -husky@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/husky/-/husky-1.3.1.tgz#26823e399300388ca2afff11cfa8a86b0033fae0" - integrity sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg== +husky@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-2.4.0.tgz#1bac7c44588f6e91f808b72efc82d24a57194f36" + integrity sha512-3k1wuZU20gFkphNWMjh2ISCFaqfbaLY7R9FST2Mj9HeRhUK9ydj9qQR8qfXlog3EctVGsyeilcZkIT7uBZDDVA== dependencies: - cosmiconfig "^5.0.7" + cosmiconfig "^5.2.0" execa "^1.0.0" find-up "^3.0.0" - get-stdin "^6.0.0" + get-stdin "^7.0.0" is-ci "^2.0.0" - pkg-dir "^3.0.0" + pkg-dir "^4.1.0" please-upgrade-node "^3.1.1" - read-pkg "^4.0.1" + read-pkg "^5.1.1" run-node "^1.0.0" - slash "^2.0.0" + slash "^3.0.0" iconv-lite@0.4.23: version "0.4.23" @@ -6997,10 +7031,10 @@ libnpx@^10.2.0: y18n "^4.0.0" yargs "^11.0.0" -lint-staged@^8.1.7: - version "8.1.7" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.1.7.tgz#a8988bc83bdffa97d04adb09dbc0b1f3a58fa6fc" - integrity sha512-egT0goFhIFoOGk6rasPngTFh2qDqxZddM0PwI58oi66RxCDcn5uDwxmiasWIF0qGnchHSYVJ8HPRD5LrFo7TKA== +lint-staged@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.2.0.tgz#3d4149a229580815c955047a7acd8f09054be5a9" + integrity sha512-DxguyxGOIfb67wZ6EOrqzjAbw6ZH9XK3YS74HO+erJf6+SAQeJJPN//GBOG5xhdt2THeuXjVPaHcCYOWGZwRbA== dependencies: chalk "^2.3.1" commander "^2.14.1" @@ -7009,7 +7043,6 @@ lint-staged@^8.1.7: dedent "^0.7.0" del "^3.0.0" execa "^1.0.0" - find-parent-dir "^0.3.0" g-status "^2.0.2" is-glob "^4.0.0" is-windows "^1.0.2" @@ -7168,6 +7201,13 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lock-verify@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.0.2.tgz#148e4f85974915c9e3c34d694b7de9ecb18ee7a8" @@ -8720,7 +8760,7 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" -p-locate@^4.0.0: +p-locate@^4.0.0, p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== @@ -9039,6 +9079,13 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + please-upgrade-node@^3.0.2, please-upgrade-node@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz#ed320051dfcc5024fae696712c8288993595e8ac" @@ -9078,10 +9125,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^1.17.1: - version "1.17.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.1.tgz#ed64b4e93e370cb8a25b9ef7fef3e4fd1c0995db" - integrity sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg== +prettier@^1.17.0, prettier@^1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" + integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== pretty-format@^24.8.0: version "24.8.0" @@ -9461,7 +9508,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -read-pkg@^4.0.0, read-pkg@^4.0.1: +read-pkg@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-4.0.1.tgz#963625378f3e1c4d48c85872b5a6ec7d5d093237" integrity sha1-ljYlN48+HE1IyFhytabsfV0JMjc= @@ -9470,7 +9517,7 @@ read-pkg@^4.0.0, read-pkg@^4.0.1: parse-json "^4.0.0" pify "^3.0.0" -read-pkg@^5.0.0: +read-pkg@^5.0.0, read-pkg@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.1.1.tgz#5cf234dde7a405c90c88a519ab73c467e9cb83f5" integrity sha512-dFcTLQi6BZ+aFUaICg7er+/usEoqFdQxiEBsEMNGoipenihtxxtdrQuBXvyANCEI8VuUIVYFgeHGx9sLLvim4w== @@ -9743,11 +9790,6 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -requireindex@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" - integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== - resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -9985,13 +10027,13 @@ rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.7.0, rollup-pluginutils@^2.8.0: dependencies: estree-walker "^0.6.1" -rollup@^1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.13.1.tgz#86a474c29df0f303ed31e4c8be5d81c1038beae8" - integrity sha512-TWBmVU5WS4wOy5Ij2qxrJYRUn/keECvStcXDpJSwgr95JZ6VFf1PDewiAk4VPf5vxr7drRJlxh9kYpxHveYOOg== +rollup@^1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.15.0.tgz#bb179a93370612ccbccfe53d09866b249a74ee04" + integrity sha512-IeZwWTqJHkZpU3zXtY3rtWkeoZc299DN8MOyNtkzlm2PpsZZLmLGlffW5giTRe7z5mhgBYvQKKpFtnnzyDOySw== dependencies: "@types/estree" "0.0.39" - "@types/node" "^12.0.3" + "@types/node" "^12.0.7" acorn "^6.1.1" rsvp@^3.3.3: @@ -11935,10 +11977,10 @@ webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== -webpack-cli@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.2.tgz#aed2437b0db0a7faa2ad28484e166a5360014a91" - integrity sha512-FLkobnaJJ+03j5eplxlI0TUxhGCOdfewspIGuvDVtpOlrAuKMFC57K42Ukxqs1tn8947/PM6tP95gQc0DCzRYA== +webpack-cli@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.4.tgz#de27e281c48a897b8c219cb093e261d5f6afe44a" + integrity sha512-ubJGQEKMtBSpT+LiL5hXvn2GIOWiRWItR1DGUqJRhwRBeGhpRXjvF5f0erqdRJLErkfqS5/Ldkkedh4AL5Q1ZQ== dependencies: chalk "^2.4.1" cross-spawn "^6.0.5" @@ -11948,6 +11990,7 @@ webpack-cli@^3.3.2: import-local "^2.0.0" interpret "^1.1.0" loader-utils "^1.1.0" + prettier "^1.17.0" supports-color "^5.5.0" v8-compile-cache "^2.0.2" yargs "^12.0.5" @@ -11960,10 +12003,10 @@ webpack-sources@^1.1.0, webpack-sources@^1.3.0: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.32.2: - version "4.32.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.32.2.tgz#3639375364a617e84b914ddb2c770aed511e5bc8" - integrity sha512-F+H2Aa1TprTQrpodRAWUMJn7A8MgDx82yQiNvYMaj3d1nv3HetKU0oqEulL9huj8enirKi8KvEXQ3QtuHF89Zg== +webpack@^4.33.0: + version "4.33.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.33.0.tgz#c30fc4307db432e5c5e3333aaa7c16a15a3b277e" + integrity sha512-ggWMb0B2QUuYso6FPZKUohOgfm+Z0sVFs8WwWuSH1IAvkWs428VDNmOlAxvHGTB9Dm/qOB/qtE5cRx5y01clxw== dependencies: "@webassemblyjs/ast" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5"