From f47885c6af83af08506fe05e255107ecf02edb22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Antunes=20Silva?= Date: Fri, 24 Apr 2020 19:15:02 -0300 Subject: [PATCH] feat: add password grant utility and fix client id and grant type (#602) * feat(oauth2): add password grant support * fix(oauth2 scheme): pass `opts` to `passwordLogin` * feat(local and refresh): properly support for client id and grant type * demo: remove client id and grant type * test: remove client id and grant type * chore(deps): add dependency requrl and remove is-https * refactor(oauth2 scheme): use requrl in `_logoutRedirectURI` method * feat(utilities): add utility `urlJoin` * refactor(oauth2 scheme): use utility `urlJoin` in `_logoutRedirectURI` method * feat(provier utils): add utility `initializePasswordGrantFlow` This remove password grant from `addAuthorize`. * refactor(laravel passport): use `initializePasswordGrantFlow` and add url error * fix(oauth2 scheme): use requrl and urlJoin in `_redirectURI` method * revert: "fix(oauth2 scheme): pass `opts` to `passwordLogin`" This reverts commit 51c7c99e * revert: "feat(oauth2): add password grant support" This reverts commit ffc3a22c * merge with dev * fix(provider utils): import requrl * chore(deps): remove dependency is-https We are using requrl * fix(laravel passport): relative url for endpoints (#633) Uses `assignAbsoluteEndpoints` utility * fix(laravel passport): update `_name` and `_scheme` to remove `_` * fix(provider utils): check if endpoint is defined in `assignAbsoluteEndpoints` * fix(laravel passport): `assignAbsoluteEndpoints` should be used right after `assingDefaults` * fix(laravel passport): fix export * fix(provider utils): check if endpoint url is defined in `assignAbsoluteEndpoints` * fix(provider utils): update `_name` to remove `_` * chore(deps): update dependency requrl to v2.0.1 * feat(utils): add utility `cleanObj` * fix(refresh scheme): fix data of refresh endpoint * demo: add laravel passport demo * fix(laravel passport): set default logout endpoint to `false` * demo(laravel passport): remove unnecessary logout endpoint Co-authored-by: pooya parsa --- demo/api/auth.js | 6 +- demo/nuxt.config.ts | 40 +++++++++++--- demo/pages/login.vue | 45 +++++++++++++++ package.json | 4 +- src/providers/_utils.ts | 92 +++++++++++++++++++++++++++---- src/providers/laravel.passport.ts | 62 ++++++++++++++++----- src/schemes/local.ts | 56 ++++--------------- src/schemes/oauth2.ts | 37 ++----------- src/schemes/refresh.ts | 68 +++++++++-------------- src/utils/index.ts | 23 +++++++- test/e2e.test.js | 6 -- test/fixture/nuxt.config.js | 7 --- yarn.lock | 10 ++-- 13 files changed, 281 insertions(+), 175 deletions(-) diff --git a/demo/api/auth.js b/demo/api/auth.js index 468975855..cacebc921 100644 --- a/demo/api/auth.js +++ b/demo/api/auth.js @@ -59,8 +59,7 @@ app.post('/login', (req, res) => { res.json({ token: { accessToken, - refreshToken, - clientId: '123' + refreshToken } }) }) @@ -86,8 +85,7 @@ app.post('/refresh', (req, res) => { refreshTokens[newRefreshToken] = { accessToken, - user, - clientId: '123' + user } res.json({ diff --git a/demo/nuxt.config.ts b/demo/nuxt.config.ts index 656e748a0..5739b04d6 100644 --- a/demo/nuxt.config.ts +++ b/demo/nuxt.config.ts @@ -47,13 +47,6 @@ export default { property: 'token.refreshToken', data: 'refreshToken', maxAge: false - }, - clientId: { - property: 'token.clientId', - data: 'clientId' - }, - grantType: { - data: 'grantType' } }, auth0: { @@ -98,6 +91,39 @@ export default { laravelSanctum: { url: '/laravel' }, + laravelPassport: { + url: 'https://laravel-auth.nuxtjs.app', + endpoints: { + userInfo: '/api/auth/passport/user' + }, + token: { + maxAge: 1800 + }, + refreshToken: { + maxAge: 60 * 60 * 24 * 30 + }, + clientId: '3', + clientSecret: 'k0NAhYGKXbG6NjENFz4VIe5YSbccZWW9V3gGeSOa' + }, + laravelPassportPasswordGrant: { + name: 'laravel.passport.password', + provider: 'laravelPassport', + url: '/laravel', + endpoints: { + user: { + url: '/api/auth/passport/user' + } + }, + token: { + maxAge: 1800 + }, + refreshToken: { + maxAge: 60 * 60 * 24 * 30 + }, + clientId: '2', + clientSecret: 'eKm1ei8muaql7TfcBxhN6Nq48oSflw6QJKCZF8gl', + grantType: 'password' + }, oauth2mock: { scheme: 'oauth2', endpoints: { diff --git a/demo/pages/login.vue b/demo/pages/login.vue index fccd45603..b9cdb46c3 100644 --- a/demo/pages/login.vue +++ b/demo/pages/login.vue @@ -84,6 +84,26 @@ Login with Laravel JWT (Test User) +
+ +
+
+ +
@@ -189,6 +209,31 @@ export default { }) }, + async loginPassport () { + this.error = null + + return this.$auth + .loginWith('laravelPassport') + .catch((e) => { + this.error = e.response ? e.response.data : e.toString() + }) + }, + + async loginPassportGrantFlow () { + this.error = null + + return this.$auth + .loginWith('laravel.passport.password', { + data: { + username: 'test@test.com', + password: '12345678' + } + }) + .catch((e) => { + this.error = e.response ? e.response.data : e.toString() + }) + }, + async loginSanctum () { this.error = null diff --git a/package.json b/package.json index 381c77f3e..c363f939d 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,11 @@ "consola": "^2.11.3", "cookie": "^0.4.1", "defu": "^2.0.2", - "is-https": "^1.0.0", "js-cookie": "^2.2.1", "jwt-decode": "^2.2.0", "lodash": "^4.17.15", - "nanoid": "^3.1.3" + "nanoid": "^3.1.3", + "requrl": "^2.0.1" }, "devDependencies": { "@nuxt/types": "latest", diff --git a/src/providers/_utils.ts b/src/providers/_utils.ts index efd140a46..5e5ab37ec 100644 --- a/src/providers/_utils.ts +++ b/src/providers/_utils.ts @@ -1,6 +1,7 @@ import defu from 'defu' import axios from 'axios' import bodyParser from 'body-parser' +import requrl from 'requrl' export function assignDefaults (strategy, defaults) { Object.assign(strategy, defu(strategy, defaults)) @@ -17,7 +18,7 @@ export function addAuthorize (nuxt, strategy) { delete strategy.clientSecret // Endpoint - const endpoint = `/_auth/oauth/${strategy._name}/authorize` + const endpoint = `/_auth/oauth/${strategy.name}/authorize` strategy.endpoints.token = endpoint // Set response_type to code @@ -80,6 +81,75 @@ export function addAuthorize (nuxt, strategy) { }) } +export function initializePasswordGrantFlow (nuxt, strategy) { + // Get clientSecret, clientId, endpoints.login.url + const clientSecret = strategy.clientSecret + const clientId = strategy.clientId + const tokenEndpoint = strategy.endpoints.token + + // IMPORTANT: remove clientSecret from generated bundle + delete strategy.clientSecret + + // Endpoint + const endpoint = `/_auth/${strategy.name}/token` + strategy.endpoints.login.url = endpoint + strategy.endpoints.refresh.url = endpoint + + // Form data parser + const formMiddleware = bodyParser.json() + + // Register endpoint + nuxt.options.serverMiddleware.unshift({ + path: endpoint, + handler: (req, res, next) => { + if (req.method !== 'POST') { + return next() + } + + formMiddleware(req, res, () => { + const { + username, + password, + grant_type: grantType = strategy.grantType, + refresh_token: refreshToken + } = req.body + + // Grant type is password, but username or password is not available + if (grantType === 'password' && (!username || !password)) { + return next(new Error('Invalid username or password')) + } + + // Grant type is refresh token, but refresh token is not available + if (grantType === 'refresh_token' && !refreshToken) { + return next(new Error('Refresh token not provided')) + } + + axios + .request({ + method: 'post', + url: tokenEndpoint, + baseURL: requrl(req), + data: { + client_id: clientId, + client_secret: clientSecret, + refresh_token: refreshToken, + grant_type: grantType, + username, + password + }, + headers: { + Accept: 'application/json' + } + }) + .then((response) => { + res.end(JSON.stringify(response.data)) + }) + .catch(error => next(error)) + }) + } + }) +} + export function assignAbsoluteEndpoints (strategy) { const { url, endpoints } = strategy @@ -87,16 +157,18 @@ export function assignAbsoluteEndpoints (strategy) { for (const key of Object.keys(endpoints)) { const endpoint = endpoints[key] - if (typeof endpoint === 'object') { - if (endpoint.url.startsWith(url)) { - continue - } - endpoints[key].url = url + endpoint.url - } else { - if (endpoint.startsWith(url)) { - continue + if (endpoint) { + if (typeof endpoint === 'object') { + if (!endpoint.url || endpoint.url.startsWith(url)) { + continue + } + endpoints[key].url = url + endpoint.url + } else { + if (endpoint.startsWith(url)) { + continue + } + endpoints[key] = url + endpoint } - endpoints[key] = url + endpoint } } } diff --git a/src/providers/laravel.passport.ts b/src/providers/laravel.passport.ts index 67d962b3a..899250721 100644 --- a/src/providers/laravel.passport.ts +++ b/src/providers/laravel.passport.ts @@ -1,18 +1,15 @@ -import { assignDefaults, addAuthorize, assignAbsoluteEndpoints } from './_utils' +import { assignDefaults, addAuthorize, initializePasswordGrantFlow, assignAbsoluteEndpoints } from './_utils' export function laravelPassport (nuxt, strategy) { - const { url } = strategy + const { url, grantType } = strategy + const isPasswordGrant = grantType === 'password' if (!url) { throw new Error('url is required is laravel passport!') } - assignDefaults(strategy, { - scheme: 'oauth2', - endpoints: { - authorization: url + '/oauth/authorize', - token: url + '/oauth/token' - }, + const defaults = { + name: 'laravel.passport', token: { property: 'access_token', type: 'Bearer', @@ -21,13 +18,50 @@ export function laravelPassport (nuxt, strategy) { }, refreshToken: { property: 'refresh_token', + data: 'refresh_token', maxAge: 60 * 60 * 24 * 30 }, - responseType: 'code', - grantType: 'authorization_code', - scope: '*' - }) + user: { + property: false + } + } + + if (isPasswordGrant) { + assignDefaults(strategy, { + ...defaults, + scheme: 'refresh', + endpoints: { + token: url + '/oauth/token', + login: {}, + refresh: {}, + logout: { + url: false + }, + user: { + url: url + '/api/auth/user' + } + }, + grantType: 'password' + }) - assignAbsoluteEndpoints(strategy) - addAuthorize(nuxt, strategy) + assignAbsoluteEndpoints(strategy) + initializePasswordGrantFlow(nuxt, strategy) + } else { + assignDefaults(strategy, { + ...defaults, + scheme: 'oauth2', + endpoints: { + authorization: url + '/oauth/authorize', + token: url + '/oauth/token', + userInfo: url + '/api/auth/user', + logout: false + }, + responseType: 'code', + grantType: 'authorization_code', + scope: '*' + }) + + assignAbsoluteEndpoints(strategy) + addAuthorize(nuxt, strategy) + } } diff --git a/src/schemes/local.ts b/src/schemes/local.ts index 7d45fe16d..05e4bba6d 100644 --- a/src/schemes/local.ts +++ b/src/schemes/local.ts @@ -31,11 +31,8 @@ const DEFAULTS: SchemeOptions = { property: 'user', autoFetch: true }, - clientId: { - property: 'client_id', - data: 'client_id', - prefix: '_client_id.' - } + clientId: false, + grantType: false } export default class LocalScheme extends BaseScheme { @@ -48,24 +45,6 @@ export default class LocalScheme extends BaseScheme { this.requestHandler = new RequestHandler(this.$auth) } - _getClientId () { - const _key = this.options.clientId.prefix + this.name - - return this.$auth.$storage.getUniversal(_key) - } - - _setClientId (clientId) { - const _key = this.options.clientId.prefix + this.name - - return this.$auth.$storage.setUniversal(_key, clientId) - } - - _syncClientId () { - const _key = this.options.clientId.prefix + this.name - - return this.$auth.$storage.syncUniversal(_key) - } - mounted () { if (this.options.token.required) { // Sync token @@ -80,10 +59,6 @@ export default class LocalScheme extends BaseScheme { } } - if (this.options.clientId) { - this._syncClientId() - } - // Initialize request interceptor this.requestHandler.initializeRequestInterceptor() @@ -107,6 +82,16 @@ export default class LocalScheme extends BaseScheme { // Ditch any leftover local tokens before attempting to log in await this.$auth.reset() + // Add client id to payload if defined + if (this.options.clientId) { + endpoint.data.client_id = this.options.clientId + } + + // Add grant type to payload if defined + if (this.options.grantType) { + endpoint.data.grant_type = this.options.grantType + } + // Make login request const response = await this.$auth.request( endpoint, @@ -119,11 +104,6 @@ export default class LocalScheme extends BaseScheme { this.$auth.token.set(token) } - // Update clientId - if (this.options.clientId) { - this._setClientId(getResponseProp(response, this.options.clientId.property)) - } - // Fetch user if (this.options.user.autoFetch) { await this.fetchUser() @@ -167,14 +147,6 @@ export default class LocalScheme extends BaseScheme { async logout (endpoint: HTTPRequest = {}) { // Only connect to logout endpoint if it's configured if (this.options.endpoints.logout) { - // Only add client id to payload if enabled - if (this.options.clientId) { - if (!endpoint.data) { - endpoint.data = {} - } - endpoint.data[this.options.clientId.data] = this._getClientId() - } - await this.$auth .requestWith(this.name, endpoint, this.options.endpoints.logout) .catch(() => { }) @@ -185,10 +157,6 @@ export default class LocalScheme extends BaseScheme { } async reset () { - if (this.options.clientId) { - this._setClientId(false) - } - this.$auth.setUser(false) this.$auth.token.reset() diff --git a/src/schemes/oauth2.ts b/src/schemes/oauth2.ts index 6a107ae30..fd1c5ca65 100644 --- a/src/schemes/oauth2.ts +++ b/src/schemes/oauth2.ts @@ -1,12 +1,11 @@ import { nanoid } from 'nanoid' -import { encodeQuery, parseQuery, normalizePath, getResponseProp } from '../utils' +import requrl from 'requrl' +import { encodeQuery, parseQuery, normalizePath, getResponseProp, urlJoin } from '../utils' import RefreshController from '../inc/refresh-controller' import RequestHandler from '../inc/request-handler' import ExpiredAuthSessionError from '../inc/expired-auth-session-error' import BaseScheme from './_scheme' -const isHttps = process.server ? require('is-https') : null - const DEFAULTS = { name: 'oauth2', accessType: null, @@ -58,39 +57,11 @@ export default class Oauth2Scheme extends BaseScheme { } get _redirectURI () { - const url = this.options.redirectUri - - if (url) { - return url - } - - if (process.server && this.req) { - const protocol = 'http' + (isHttps(this.req) ? 's' : '') + '://' - - return protocol + this.req.headers.host + this.$auth.options.redirect.callback - } - - if (process.client) { - return window.location.origin + this.$auth.options.redirect.callback - } + return this.options.redirectUri || urlJoin(requrl(this.req), this.$auth.options.redirect.callback) } get _logoutRedirectURI () { - const url = this.options.logoutRedirectUri - - if (url) { - return url - } - - if (process.server && this.req) { - const protocol = 'http' + (isHttps(this.req) ? 's' : '') + '://' - - return protocol + this.req.headers.host + this.$auth.options.redirect.logout - } - - if (process.client) { - return window.location.origin + this.$auth.options.redirect.logout - } + return this.options.logoutRedirectUri || urlJoin(requrl(this.req), this.$auth.options.redirect.logout) } async mounted () { diff --git a/src/schemes/refresh.ts b/src/schemes/refresh.ts index 81a804103..b88dc2e04 100644 --- a/src/schemes/refresh.ts +++ b/src/schemes/refresh.ts @@ -1,4 +1,4 @@ -import { getResponseProp } from '../utils' +import { cleanObj, getResponseProp } from '../utils' import RefreshController from '../inc/refresh-controller' import ExpiredAuthSessionError from '../inc/expired-auth-session-error' import LocalScheme from './local' @@ -41,15 +41,8 @@ const DEFAULTS = { property: 'user', autoFetch: true }, - clientId: { - property: 'client_id', - data: 'client_id', - prefix: '_client_id.' - }, - grantType: { - data: 'grant_type', - value: 'refresh_token' - }, + clientId: false, + grantType: false, autoLogout: false } @@ -68,11 +61,6 @@ export default class RefreshScheme extends LocalScheme { this.$auth.token.sync() this.$auth.refreshToken.sync() - // Sync client id - if (this.options.clientId) { - this._syncClientId() - } - // Get token and refresh token status const tokenStatus = this.$auth.token.status() const refreshTokenStatus = this.$auth.refreshToken.status() @@ -113,6 +101,16 @@ export default class RefreshScheme extends LocalScheme { // Ditch any leftover local tokens before attempting to log in await this.$auth.reset() + // Add client id to payload if enabled + if (this.options.clientId) { + endpoint.data.client_id = this.options.clientId + } + + // Add grant type to payload if defined + if (this.options.grantType) { + endpoint.data.grant_type = this.options.grantType + } + // Make login request const response = await this.$auth.request( endpoint, @@ -126,11 +124,6 @@ export default class RefreshScheme extends LocalScheme { this.$auth.token.set(token) this.$auth.refreshToken.set(refreshToken) - // Update client id - if (this.options.clientId) { - this._setClientId(getResponseProp(response, this.options.clientId.property)) - } - // Fetch user if `autoFetch` is enabled if (this.options.user.autoFetch) { await this.fetchUser() @@ -179,28 +172,29 @@ export default class RefreshScheme extends LocalScheme { throw new ExpiredAuthSessionError() } - const endpoint = { data: null } - const data = {} + const endpoint = { + data: { + client_id: undefined, + grant_type: undefined + } + } - // Only add refresh token to payload if required + // Add refresh token to payload if required if (this.options.refreshToken.required) { - data[this.options.refreshToken.data] = this.$auth.refreshToken.get() + endpoint.data[this.options.refreshToken.data] = this.$auth.refreshToken.get() } - // Only add client id to payload if enabled + // Add client id to payload if defined if (this.options.clientId) { - data[this.options.clientId.data] = this._getClientId() + endpoint.data.client_id = this.options.clientId } - // Only add grant type to payload if enabled + // Add grant type to payload if defined if (this.options.grantType) { - data[this.options.grantType.data] = this.options.grantType.value + endpoint.data.grant_type = 'refresh_token' } - // Add data to endpoint - if (Object.keys(data).length) { - endpoint.data = data - } + cleanObj(endpoint.data) // Make refresh request return this.$auth.request( @@ -217,12 +211,6 @@ export default class RefreshScheme extends LocalScheme { this.$auth.refreshToken.set(refreshToken) } - // Update client id - const clientId = getResponseProp(response, this.options.clientId.property) - if (this.options.clientId && clientId) { - this._setClientId(clientId) - } - return response }).catch((error) => { this.$auth.callOnError(error, { method: 'refreshToken' }) @@ -230,10 +218,6 @@ export default class RefreshScheme extends LocalScheme { } async reset () { - if (this.options.clientId) { - this._setClientId(false) - } - this.$auth.setUser(false) this.$auth.token.reset() this.$auth.refreshToken.reset() diff --git a/src/utils/index.ts b/src/utils/index.ts index 84f4d0d78..8a7746c9f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -83,7 +83,8 @@ export function decodeValue (val) { if (typeof val === 'string') { try { return JSON.parse(val) - } catch (_) { } + } catch (_) { + } } // Return as is @@ -133,3 +134,23 @@ export function addTokenPrefix (token, tokenType) { return tokenType + ' ' + token } + +export function urlJoin (...args) { + return args.join('/') + .replace(/[/]+/g, '/') + .replace(/^(.+):\//, '$1://') + .replace(/^file:/, 'file:/') + .replace(/\/(\?|&|#[^!])/g, '$1') + .replace(/\?/g, '&') + .replace('&', '?') +} + +export function cleanObj (obj) { + for (const key in obj) { + if (obj[key] === undefined) { + delete obj[key] + } + } + + return obj +} diff --git a/test/e2e.test.js b/test/e2e.test.js index 6f19981d6..1510b9819 100644 --- a/test/e2e.test.js +++ b/test/e2e.test.js @@ -75,7 +75,6 @@ describe('e2e', () => { loginToken, loginRefreshToken, loginExpiresAt, - loginClientId, loginUser, loginAxiosBearer, loginResponse @@ -89,7 +88,6 @@ describe('e2e', () => { loginToken: window.$nuxt.$auth.token.get(), loginRefreshToken: window.$nuxt.$auth.refreshToken.get(), loginExpiresAt: window.$nuxt.$auth.token._getExpiration(), - loginClientId: window.$nuxt.$auth.strategy._getClientId(), loginUser: window.$nuxt.$auth.user, loginResponse } @@ -101,7 +99,6 @@ describe('e2e', () => { expect(loginToken).toBeDefined() expect(loginRefreshToken).toBeDefined() expect(loginExpiresAt).toBeDefined() - expect(loginClientId).toBe(123) expect(loginUser).toBeDefined() expect(loginUser.username).toBe('test_username') expect(loginResponse).toBeDefined() @@ -111,7 +108,6 @@ describe('e2e', () => { refreshedRefreshToken, refreshedExpiresAt, refreshedAxiosBearer, - refreshedClientId, refreshedUser, refreshedResponse } = await page.evaluate(async () => { @@ -122,7 +118,6 @@ describe('e2e', () => { refreshedToken: window.$nuxt.$auth.token.get(), refreshedRefreshToken: window.$nuxt.$auth.refreshToken.get(), refreshedExpiresAt: window.$nuxt.$auth.token._getExpiration(), - refreshedClientId: window.$nuxt.$auth.strategy._getClientId(), refreshedUser: window.$nuxt.$auth.user, refreshedResponse } @@ -138,7 +133,6 @@ describe('e2e', () => { expect(refreshedRefreshToken).not.toEqual(loginRefreshToken) expect(refreshedExpiresAt).toBeDefined() expect(refreshedExpiresAt).toBeGreaterThanOrEqual(loginExpiresAt) - expect(refreshedClientId).toBe(123) expect(refreshedUser).toBeDefined() expect(refreshedUser.username).toBe('test_username') expect(refreshedResponse).toBeDefined() diff --git a/test/fixture/nuxt.config.js b/test/fixture/nuxt.config.js index 5c994d36e..12583dc1f 100644 --- a/test/fixture/nuxt.config.js +++ b/test/fixture/nuxt.config.js @@ -35,13 +35,6 @@ export default { property: 'token.refreshToken', data: 'refreshToken', maxAge: false - }, - clientId: { - property: 'token.clientId', - data: 'clientId' - }, - grantType: { - data: 'grantType' } }, test: { diff --git a/yarn.lock b/yarn.lock index a47791fcd..460155fe6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5924,11 +5924,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-https@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-https/-/is-https-1.0.0.tgz#9c1dde000dc7e7288edb983bef379e498e7cb1bf" - integrity sha512-1adLLwZT9XEXjzhQhZxd75uxf0l+xI9uTSFaZeSESjL3E1eXSPpO+u5RcgqtzeZ1KCaNvtEwZSTO2P4U5erVqQ== - is-nan@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03" @@ -9308,6 +9303,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +requrl@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/requrl/-/requrl-2.0.1.tgz#7028de9f00bf87b35585c675e94d75987f6973e9" + integrity sha512-l6iZwt1x7CN4TCyZqIbYS1o3tN5FRGnsvSSBOrtsmgS87f48J81AvRv6HGri79RHW4Ssim07fr+Fw08cYMWmsw== + reserved-words@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"