Merge "Support auth in multiple tabs"
This commit is contained in:
commit
7f3aede03f
|
@ -12,6 +12,7 @@
|
|||
"@patternfly/react-table": "4.29.58",
|
||||
"@softwarefactory-project/re-ansi": "^0.4.0",
|
||||
"axios": "^0.19.0",
|
||||
"broadcast-channel": "^4.5.0",
|
||||
"js-yaml": "^3.13.0",
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.2",
|
||||
|
|
|
@ -17,6 +17,7 @@ import { create, act } from 'react-test-renderer'
|
|||
import ReactDOM from 'react-dom'
|
||||
import { Link, BrowserRouter as Router } from 'react-router-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { BroadcastChannel, createLeaderElection } from 'broadcast-channel'
|
||||
|
||||
import { fetchInfoIfNeeded } from './actions/info'
|
||||
import configureStore from './store'
|
||||
|
@ -32,20 +33,27 @@ api.fetchStatus = jest.fn()
|
|||
api.fetchConfigErrors = jest.fn()
|
||||
api.fetchConfigErrors.mockImplementation(() => Promise.resolve({data: []}))
|
||||
|
||||
|
||||
it('renders without crashing', () => {
|
||||
it('renders without crashing', async () => {
|
||||
const store = configureStore()
|
||||
const channel = new BroadcastChannel('zuul')
|
||||
const auth_election = createLeaderElection(channel)
|
||||
const div = document.createElement('div')
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<ZuulAuthProvider><Router><App /></Router></ZuulAuthProvider>
|
||||
<ZuulAuthProvider channel={channel} election={auth_election}>
|
||||
<Router><App /></Router>
|
||||
</ZuulAuthProvider>
|
||||
</Provider>,
|
||||
div)
|
||||
ReactDOM.unmountComponentAtNode(div)
|
||||
await auth_election.die()
|
||||
await channel.close()
|
||||
})
|
||||
|
||||
it('renders multi tenant', async () => {
|
||||
const store = configureStore()
|
||||
const channel = new BroadcastChannel('zuul')
|
||||
const auth_election = createLeaderElection(channel)
|
||||
api.fetchInfo.mockImplementation(
|
||||
() => Promise.resolve({data: {
|
||||
info: {capabilities: {}}
|
||||
|
@ -57,7 +65,9 @@ it('renders multi tenant', async () => {
|
|||
|
||||
const application = create(
|
||||
<Provider store={store}>
|
||||
<ZuulAuthProvider><Router><App /></Router></ZuulAuthProvider>
|
||||
<ZuulAuthProvider channel={channel} election={auth_election}>
|
||||
<Router><App /></Router>
|
||||
</ZuulAuthProvider>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
|
@ -80,10 +90,14 @@ it('renders multi tenant', async () => {
|
|||
expect(application.root.findAllByType(TenantsPage)).not.toEqual(null)
|
||||
// Fetch tenants has been called
|
||||
expect(api.fetchTenants).toBeCalled()
|
||||
await auth_election.die()
|
||||
await channel.close()
|
||||
})
|
||||
|
||||
it('renders single tenant', async () => {
|
||||
const store = configureStore()
|
||||
const channel = new BroadcastChannel('zuul')
|
||||
const auth_election = createLeaderElection(channel)
|
||||
api.fetchInfo.mockImplementation(
|
||||
() => Promise.resolve({data: {
|
||||
info: {capabilities: {}, tenant: 'openstack'}
|
||||
|
@ -95,7 +109,9 @@ it('renders single tenant', async () => {
|
|||
|
||||
const application = create(
|
||||
<Provider store={store}>
|
||||
<ZuulAuthProvider><Router><App /></Router></ZuulAuthProvider>
|
||||
<ZuulAuthProvider channel={channel} election={auth_election}>
|
||||
<Router><App /></Router>
|
||||
</ZuulAuthProvider>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
|
@ -116,4 +132,6 @@ it('renders single tenant', async () => {
|
|||
expect(application.root.findAllByType(StatusPage)).not.toEqual(null)
|
||||
// Fetch status has been called
|
||||
expect(api.fetchStatus).toBeCalled()
|
||||
await auth_election.die()
|
||||
await channel.close()
|
||||
})
|
||||
|
|
|
@ -19,6 +19,9 @@ import { connect } from 'react-redux'
|
|||
|
||||
import { AuthProvider } from 'oidc-react'
|
||||
import { userLoggedIn, userLoggedOut } from './actions/user'
|
||||
import { UserManager, User } from 'oidc-client'
|
||||
import { getHomepageUrl } from './api'
|
||||
import _ from 'lodash'
|
||||
|
||||
|
||||
class ZuulAuthProvider extends React.Component {
|
||||
|
@ -41,26 +44,136 @@ class ZuulAuthProvider extends React.Component {
|
|||
*/
|
||||
static propTypes = {
|
||||
auth_params: PropTypes.object,
|
||||
channel: PropTypes.object,
|
||||
election: PropTypes.object,
|
||||
dispatch: PropTypes.func,
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { auth_params } = this.props
|
||||
const { auth_params, channel, election } = this.props
|
||||
|
||||
console.debug('ZuulAuthProvider rendering with params', auth_params)
|
||||
|
||||
const userManager = new UserManager({
|
||||
...auth_params,
|
||||
response_type: 'token id_token',
|
||||
silent_redirect_uri: getHomepageUrl() + 'silent_callback',
|
||||
redirect_uri: getHomepageUrl() + 'auth_callback',
|
||||
includeIdTokenInSilentRenew: false,
|
||||
})
|
||||
|
||||
const oidcConfig = {
|
||||
onSignIn: async (user) => {
|
||||
// Update redux with the logged in state and send the
|
||||
// credentials to any other tabs.
|
||||
this.props.dispatch(userLoggedIn(user))
|
||||
this.props.channel.postMessage({
|
||||
type: 'signIn',
|
||||
auth_params: auth_params,
|
||||
user: user
|
||||
})
|
||||
},
|
||||
onSignOut: async () => {
|
||||
// Update redux with the logged out state and send the
|
||||
// credentials to any other tabs.
|
||||
this.props.dispatch(userLoggedOut())
|
||||
this.props.channel.postMessage({
|
||||
type: 'signOut',
|
||||
auth_params: auth_params
|
||||
})
|
||||
},
|
||||
responseType: 'token id_token',
|
||||
autoSignIn: false,
|
||||
...auth_params,
|
||||
userManager: userManager,
|
||||
}
|
||||
|
||||
// This is called whenever we receive a message from another tab
|
||||
channel.onmessage = (msg) => {
|
||||
console.debug('Received broadcast message', msg)
|
||||
|
||||
if (msg.type === 'signIn' && _.isEqual(msg.auth_params, auth_params)) {
|
||||
const user = new User(msg.user)
|
||||
userManager.getUser().then((olduser) => {
|
||||
// In some cases, we can receive our own message, so make
|
||||
// sure that the user info we received is different from
|
||||
// what we already have.
|
||||
let needToUpdate = true
|
||||
if (olduser) {
|
||||
if (user.toStorageString() === olduser.toStorageString()) {
|
||||
needToUpdate = false
|
||||
}
|
||||
}
|
||||
if (needToUpdate) {
|
||||
console.debug('New token from other tab')
|
||||
userManager.storeUser(user)
|
||||
userManager.events.load(user)
|
||||
this.props.dispatch(userLoggedIn(user))
|
||||
}
|
||||
})
|
||||
}
|
||||
else if (msg.type === 'signOut' && _.isEqual(msg.auth_params, auth_params)) {
|
||||
userManager.removeUser()
|
||||
this.props.dispatch(userLoggedOut())
|
||||
}
|
||||
else if (msg.type === 'init') {
|
||||
// A new tab has been created; send our token in case it's helpful.
|
||||
userManager.getUser().then((user) => {
|
||||
if (user) {
|
||||
console.debug('Sending token to new tab')
|
||||
this.props.channel.postMessage({
|
||||
type: 'signIn',
|
||||
auth_params: auth_params,
|
||||
user: user
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If we already have user data saved in session storage, we need to
|
||||
// tell redux about it.
|
||||
userManager.getUser().then((user) => {
|
||||
if (user) {
|
||||
console.debug('Restoring initial login from userManager')
|
||||
this.props.dispatch(userLoggedIn(user))
|
||||
} else {
|
||||
// Maybe another tab is logged in. Ask them to send us tokens.
|
||||
console.debug('Asking other tabs for auth tokens')
|
||||
this.props.channel.postMessage({ type: 'init' })
|
||||
}
|
||||
})
|
||||
|
||||
// This is called when a token is expired
|
||||
userManager.events.addAccessTokenExpired(() => {
|
||||
console.log('Auth token expired')
|
||||
userManager.removeUser()
|
||||
this.props.dispatch(userLoggedOut())
|
||||
this.props.channel.postMessage({ 'type': 'signOut' })
|
||||
})
|
||||
|
||||
// This is called about 1 minute before a token is expired. We will try
|
||||
// to renew the token. We use a leader election so that only one tab
|
||||
// makes the attempt; the others will receive the token via a broadcast
|
||||
// event.
|
||||
userManager.events.addAccessTokenExpiring(() => {
|
||||
if (election.isLeader) {
|
||||
console.debug('Token is expiring; renewing')
|
||||
userManager.signinSilent().then(user => {
|
||||
console.debug('Token renewal successful')
|
||||
this.props.dispatch(userLoggedIn(user))
|
||||
channel.postMessage({
|
||||
type: 'signIn',
|
||||
auth_params: auth_params,
|
||||
user: user
|
||||
})
|
||||
}, err => {
|
||||
console.error('Error renewing token:', err.message)
|
||||
})
|
||||
} else {
|
||||
console.debug('Token is expiring; expecting leader to renew')
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AuthProvider {...oidcConfig} key={JSON.stringify(auth_params)}>
|
||||
|
|
|
@ -34,7 +34,7 @@ function createAuthParamsFromJson(json) {
|
|||
|
||||
let auth_params = {
|
||||
authority: '',
|
||||
clientId: '',
|
||||
client_id: '',
|
||||
scope: '',
|
||||
}
|
||||
if (!auth_info) {
|
||||
|
@ -44,7 +44,7 @@ function createAuthParamsFromJson(json) {
|
|||
const realm = auth_info.default_realm
|
||||
const client_config = auth_info.realms[realm]
|
||||
if (client_config.driver === 'OpenIDConnect') {
|
||||
auth_params.clientId = client_config.client_id
|
||||
auth_params.client_id = client_config.client_id
|
||||
auth_params.scope = client_config.scope
|
||||
auth_params.authority = client_config.authority
|
||||
return auth_params
|
||||
|
|
|
@ -41,7 +41,6 @@ import { apiUrl } from '../../api'
|
|||
import { fetchUserACL } from '../../actions/user'
|
||||
import { withAuth } from 'oidc-react'
|
||||
import { getHomepageUrl } from '../../api'
|
||||
import { userLoggedIn, userLoggedOut } from '../../actions/user'
|
||||
|
||||
|
||||
class AuthContainer extends React.Component {
|
||||
|
@ -55,7 +54,6 @@ class AuthContainer extends React.Component {
|
|||
// Props coming from withAuth
|
||||
signIn: PropTypes.func,
|
||||
signOut: PropTypes.func,
|
||||
userData: PropTypes.object,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -76,30 +74,11 @@ class AuthContainer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { user, userData } = this.props
|
||||
|
||||
// Make sure redux is synced with the userManager
|
||||
const now = Date.now() / 1000
|
||||
if (userData && userData.expires_at < now) {
|
||||
console.log('Token expired, logging out')
|
||||
this.props.signOut()
|
||||
this.props.dispatch(userLoggedOut())
|
||||
} else if (user.data !== userData) {
|
||||
console.log('Restoring login from userManager')
|
||||
this.props.dispatch(userLoggedIn(userData))
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { user, tenant } = this.props
|
||||
|
||||
// Make sure the token is current and the tenant is up to date.
|
||||
const now = Date.now() / 1000
|
||||
if (user.data && user.data.expires_at < now) {
|
||||
console.log('Token expired, logging out')
|
||||
this.props.signOut()
|
||||
} else if (user.data && user.tenant !== tenant.name) {
|
||||
if (user.data && user.tenant !== tenant.name) {
|
||||
console.log('Refreshing ACL', user.tenant, tenant.name)
|
||||
this.props.dispatch(fetchUserACL(tenant.name, user))
|
||||
}
|
||||
|
@ -188,7 +167,7 @@ class AuthContainer extends React.Component {
|
|||
onClick={() => {
|
||||
const redirect_target = window.location.href.slice(getHomepageUrl().length)
|
||||
localStorage.setItem('zuul_auth_redirect', redirect_target)
|
||||
this.props.signIn({ redirect_uri: getHomepageUrl() + 'auth_callback' })
|
||||
this.props.signIn()
|
||||
}}>
|
||||
Sign in
|
||||
<SignInAltIcon title='Sign In' />
|
||||
|
|
|
@ -19,6 +19,7 @@ import React from 'react'
|
|||
import ReactDOM from 'react-dom'
|
||||
import { BrowserRouter as Router } from 'react-router-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { BroadcastChannel, createLeaderElection } from 'broadcast-channel'
|
||||
import 'patternfly/dist/css/patternfly.min.css'
|
||||
import 'patternfly/dist/css/patternfly-additions.min.css'
|
||||
// NOTE (felix): The Patternfly 4 CSS file must be imported before the App
|
||||
|
@ -47,16 +48,48 @@ import App from './App'
|
|||
// is imported within the App).
|
||||
import './index.css'
|
||||
import ZuulAuthProvider from './ZuulAuthProvider'
|
||||
import SilentCallback from './pages/SilentCallback'
|
||||
|
||||
const store = configureStore()
|
||||
// Uncomment the next 3 lines to enable debug-level logging from
|
||||
// oidc-client.
|
||||
// import { Log } from 'oidc-client'
|
||||
// Log.logger = console
|
||||
// Log.level = Log.DEBUG
|
||||
|
||||
// Load info endpoint
|
||||
store.dispatch(fetchInfoIfNeeded())
|
||||
// Don't render the entire application to handle a silent
|
||||
// authentication callback.
|
||||
if ((window.location.origin + window.location.pathname) ===
|
||||
(getHomepageUrl() + 'silent_callback')) {
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<ZuulAuthProvider>
|
||||
<Router basename={new URL(getHomepageUrl()).pathname}><App /></Router>
|
||||
</ZuulAuthProvider>
|
||||
</Provider>, document.getElementById('root'))
|
||||
registerServiceWorker()
|
||||
ReactDOM.render(
|
||||
<SilentCallback/>,
|
||||
document.getElementById('root'))
|
||||
|
||||
} else {
|
||||
|
||||
const store = configureStore()
|
||||
|
||||
// Load info endpoint
|
||||
store.dispatch(fetchInfoIfNeeded())
|
||||
|
||||
// Create a broadcast channel for sending auth (or other)
|
||||
// information between tabs.
|
||||
const channel = new BroadcastChannel('zuul')
|
||||
|
||||
// Create an election so that only one tab will renew auth tokens. We run the
|
||||
// election perpetually and just check whether we are the leader when it's time
|
||||
// to renew tokens.
|
||||
const auth_election = createLeaderElection(channel)
|
||||
const waitForever = new Promise(function () {})
|
||||
auth_election.awaitLeadership().then(()=> {
|
||||
waitForever.then(function() {})
|
||||
})
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<ZuulAuthProvider channel={channel} election={auth_election}>
|
||||
<Router basename={new URL(getHomepageUrl()).pathname}><App /></Router>
|
||||
</ZuulAuthProvider>
|
||||
</Provider>, document.getElementById('root'))
|
||||
registerServiceWorker()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2021 Acme Gating, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
// This is an abbreviated rendering of the app which only renders the
|
||||
// AuthProvider. This is rendered in a hidden iframe during
|
||||
// authentication token renewal. We don't need to render the entire
|
||||
// app, and we also don't need to render the AuthProvider with all of
|
||||
// our settings.
|
||||
|
||||
import React from 'react'
|
||||
import { AuthProvider } from 'oidc-react'
|
||||
import { UserManager } from 'oidc-client'
|
||||
|
||||
class SilentCallback extends React.Component {
|
||||
render() {
|
||||
const oidcConfig = {
|
||||
autoSignIn: false,
|
||||
userManager: new UserManager(),
|
||||
}
|
||||
|
||||
return <AuthProvider {...oidcConfig}/>
|
||||
}
|
||||
}
|
||||
|
||||
export default SilentCallback
|
|
@ -24,7 +24,7 @@ import {
|
|||
const stored_params = localStorage.getItem('zuul_auth_params')
|
||||
let auth_params = {
|
||||
authority: '',
|
||||
clientId: '',
|
||||
client_id: '',
|
||||
scope: '',
|
||||
}
|
||||
if (stored_params !== null) {
|
||||
|
|
|
@ -1017,6 +1017,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.16.0", "@babel/runtime@^7.6.2":
|
||||
version "7.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
|
||||
version "7.8.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
|
||||
|
@ -3080,6 +3087,11 @@ before-after-hook@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635"
|
||||
integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==
|
||||
|
||||
big-integer@^1.6.16:
|
||||
version "1.6.51"
|
||||
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686"
|
||||
integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==
|
||||
|
||||
big.js@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
|
@ -3260,6 +3272,19 @@ breakjs@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/breakjs/-/breakjs-1.0.0.tgz#ec8353a06862eb43962deae09072ee66a4cd8459"
|
||||
integrity sha1-7INToGhi60OWLergkHLuZqTNhFk=
|
||||
|
||||
broadcast-channel@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.5.0.tgz#d4717c493e219908fcb7f2f9078fe0baf95b77c1"
|
||||
integrity sha512-jp+VPlQ1HyR0CM3uIYUrdpXupBvhTMFRkjR6mEmt5W4HaGDPFEzrO2Jqvi2PZ6zCC4zwLeco7CC5EUJPrVH8Tw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.16.0"
|
||||
detect-node "^2.1.0"
|
||||
microseconds "0.2.0"
|
||||
nano-time "1.0.0"
|
||||
oblivious-set "1.0.0"
|
||||
rimraf "3.0.2"
|
||||
unload "2.3.1"
|
||||
|
||||
brorand@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||
|
@ -5039,6 +5064,11 @@ detect-newline@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
|
||||
integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
|
||||
|
||||
detect-node@2.1.0, detect-node@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
|
||||
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
|
||||
|
||||
detect-node@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
|
||||
|
@ -9418,6 +9448,11 @@ micromatch@^4.0.2:
|
|||
braces "^3.0.1"
|
||||
picomatch "^2.0.5"
|
||||
|
||||
microseconds@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
|
||||
integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
|
||||
|
||||
miller-rabin@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
|
||||
|
@ -9690,6 +9725,13 @@ nan@^2.12.1:
|
|||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
|
||||
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
|
||||
|
||||
nano-time@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
|
||||
integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=
|
||||
dependencies:
|
||||
big-integer "^1.6.16"
|
||||
|
||||
nanoid@2.1.11:
|
||||
version "2.1.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
|
||||
|
@ -10297,6 +10339,11 @@ object.values@^1.1.0, object.values@^1.1.1:
|
|||
function-bind "^1.1.1"
|
||||
has "^1.0.3"
|
||||
|
||||
oblivious-set@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566"
|
||||
integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==
|
||||
|
||||
obuf@^1.0.0, obuf@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
|
||||
|
@ -13090,6 +13137,13 @@ rimraf@2.6.3:
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
|
@ -14735,6 +14789,14 @@ universalify@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||
|
||||
unload@2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/unload/-/unload-2.3.1.tgz#9d16862d372a5ce5cb630ad1309c2fd6e35dacfe"
|
||||
integrity sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
detect-node "2.1.0"
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
|
Loading…
Reference in New Issue