Use OAuth2 to login on firefox-ci Taskcluster instance (#160)
This commit is contained in:
Родитель
53302b7035
Коммит
d00763e8bd
|
@ -101,7 +101,7 @@ The authentication configuration has the following characteristics:
|
|||
* There are two different Auth0 clients
|
||||
* An official one (SSO + LDAP) and the other for non-LDAP contributors
|
||||
* Non-LDAP users will receive fake org data
|
||||
* After a user authenticates, the auth will also authenticate with Taskcluster (`login.taskcluster.net`)
|
||||
* After a user authenticates, the auth will also authenticate with Firefox CI Taskcluster (`firefox-ci-tc.services.mozilla.com`)
|
||||
* This is in order to later fetch a Taskcluster secret (only available to LDAP users)
|
||||
|
||||
## Running & tests
|
||||
|
|
|
@ -29,9 +29,11 @@
|
|||
"@mozilla-frontend-infra/components": "^2.0.0",
|
||||
"auth0-js": "9.6.1",
|
||||
"chart.js": "^2.7.3",
|
||||
"client-oauth2": "^4.2.5",
|
||||
"mitt": "^1.1.3",
|
||||
"moment": "^2.23.0",
|
||||
"mui-datatables": "^2.4.0",
|
||||
"pako": "^1.0.10",
|
||||
"prop-types": "^15",
|
||||
"query-string": "^6.2.0",
|
||||
"react": "^16",
|
||||
|
@ -41,8 +43,7 @@
|
|||
"react-router-dom": "^4.3.1",
|
||||
"taskcluster-client-web": "9.0.0",
|
||||
"taskcluster-lib-urls": "^12.0.0",
|
||||
"typeface-roboto": "^0.0.54",
|
||||
"pako": "^1.0.10"
|
||||
"typeface-roboto": "^0.0.54"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@neutrinojs/airbnb": "^9.0.0-rc.1",
|
||||
|
|
|
@ -16,8 +16,7 @@ import PropsRoute from '../components/PropsRoute';
|
|||
import AuthContext from '../components/auth/AuthContext';
|
||||
import AuthController from '../components/auth/AuthController';
|
||||
import NotFound from '../components/NotFound';
|
||||
import Auth0Login from '../views/Auth0Login';
|
||||
import config from '../config';
|
||||
import OAuth2Login from '../views/OAuth2Login';
|
||||
|
||||
const styles = () => ({
|
||||
'@global': {
|
||||
|
@ -67,9 +66,15 @@ class App extends React.Component {
|
|||
this.handleUserSessionChanged,
|
||||
);
|
||||
|
||||
// Start the Oauth code exchange when it hass received as /?code=XXX
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('code') !== null) {
|
||||
this.authController.exchangeCode(window.location.href);
|
||||
}
|
||||
|
||||
// we do not want to automatically load a user session on the login views; this is
|
||||
// a hack until they get an entry point of their own with no UI.
|
||||
if (!window.location.pathname.startsWith(config.redirectRoute)) {
|
||||
if (!window.location.pathname.startsWith('/login')) {
|
||||
this.authController.loadUserSession();
|
||||
} else {
|
||||
this.setState({ authReady: true });
|
||||
|
@ -91,8 +96,8 @@ class App extends React.Component {
|
|||
<Redirect to="/reportees" />
|
||||
</Route>
|
||||
<PropsRoute
|
||||
path={config.redirectRoute}
|
||||
component={Auth0Login}
|
||||
path="/login"
|
||||
component={OAuth2Login}
|
||||
setUserSession={this.authController.setUserSession}
|
||||
/>
|
||||
<PropsRoute path="/" component={Main} />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import mitt from 'mitt';
|
||||
import UserSession from './UserSession';
|
||||
|
||||
import { renew as auth0Renew } from './auth0';
|
||||
import { userSessionFromCode, renew as auth0Renew } from './oauth2';
|
||||
|
||||
/**
|
||||
* Controller for authentication-related pieces of the site.
|
||||
|
@ -53,6 +53,14 @@ export default class AuthController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange Oauth Code received in URL callback
|
||||
* and build a User Session from Taskcluster credentials
|
||||
*/
|
||||
async exchangeCode(url) {
|
||||
this.setUserSession(await userSessionFromCode(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the current user session (from localStorage).
|
||||
*
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { OIDCCredentialAgent, Secrets, Index } from 'taskcluster-client-web';
|
||||
import { withRootUrl } from 'taskcluster-lib-urls';
|
||||
import { Secrets, Index } from 'taskcluster-client-web';
|
||||
import { TASKCLUSTER_ROOT_URL } from '../../config';
|
||||
|
||||
const urls = withRootUrl('https://taskcluster.net');
|
||||
/**
|
||||
* An object representing a user session. Tools supports a variety of login methods,
|
||||
* so this combines them all in a single representation.
|
||||
|
@ -18,14 +17,6 @@ const urls = withRootUrl('https://taskcluster.net');
|
|||
* - renewAfter - date (Date or string) after which this session should be renewed,
|
||||
* if applicable
|
||||
*
|
||||
* When type is 'oidc':
|
||||
*
|
||||
* - oidcProvider -- the provider (see taskcluster-login)
|
||||
* - accessToken -- the accessToken to pass to taskcluster-login
|
||||
* - fullName -- user's full name
|
||||
* - picture -- URL of an image of the user
|
||||
* - oidcSubject -- the 'sub' field of the id_token (useful for debugging user issues)
|
||||
*
|
||||
* When the type is 'credentials':
|
||||
*
|
||||
* - credentials -- the Taskcluster credentials (with or without a certificate)
|
||||
|
@ -36,21 +27,10 @@ const urls = withRootUrl('https://taskcluster.net');
|
|||
export default class UserSession {
|
||||
constructor(options) {
|
||||
Object.assign(this, options);
|
||||
|
||||
if (this.accessToken) {
|
||||
this.credentialAgent = new OIDCCredentialAgent({
|
||||
accessToken: this.accessToken,
|
||||
url: urls.api('login', 'v1', `/oidc-credentials/${this.oidcProvider}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static fromCredentials(credentials) {
|
||||
return new UserSession({ type: 'credentials', credentials });
|
||||
}
|
||||
|
||||
static fromOIDC(options) {
|
||||
return new UserSession({ type: 'oidc', ...options });
|
||||
return new UserSession({ type: 'credentials', email: 'nobody@mozilla.org', credentials });
|
||||
}
|
||||
|
||||
// determine whether the user changed from old to new; this is used by other components
|
||||
|
@ -78,9 +58,7 @@ export default class UserSession {
|
|||
|
||||
// get the args used to create a new client object
|
||||
get clientArgs() {
|
||||
return this.credentialAgent
|
||||
? { credentialAgent: this.credentialAgent }
|
||||
: { credentials: this.credentials };
|
||||
return { credentials: this.credentials };
|
||||
}
|
||||
|
||||
// load Taskcluster credentials for this user
|
||||
|
@ -98,7 +76,13 @@ export default class UserSession {
|
|||
return JSON.stringify({ ...this, credentialAgent: undefined });
|
||||
}
|
||||
|
||||
getTaskClusterSecretsClient = () => new Secrets({ ...this.clientArgs, rootUrl: 'https://taskcluster.net' });
|
||||
getTaskClusterSecretsClient = () => new Secrets({
|
||||
...this.clientArgs,
|
||||
rootUrl: TASKCLUSTER_ROOT_URL,
|
||||
});
|
||||
|
||||
getTaskClusterIndexClient = () => new Index({ ...this.clientArgs, rootUrl: 'https://taskcluster.net' });
|
||||
getTaskClusterIndexClient = () => new Index({
|
||||
...this.clientArgs,
|
||||
rootUrl: TASKCLUSTER_ROOT_URL,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import { fromNow } from 'taskcluster-client-web';
|
||||
import { WebAuth } from 'auth0-js';
|
||||
import UserSession from './UserSession';
|
||||
import config from '../../config';
|
||||
|
||||
export const webAuth = new WebAuth(config.auth0Options);
|
||||
|
||||
export function userSessionFromAuthResult(authResult) {
|
||||
return UserSession.fromOIDC({
|
||||
oidcProvider: 'mozilla-auth0',
|
||||
accessToken: authResult.accessToken,
|
||||
fullName: authResult.idTokenPayload.name === '' ? authResult.idTokenPayload.nickname : authResult.idTokenPayload.name,
|
||||
email: authResult.idTokenPayload.email,
|
||||
picture: authResult.idTokenPayload.picture,
|
||||
oidcSubject: authResult.idTokenPayload.sub,
|
||||
// per https://wiki.mozilla.org/Security/Guidelines/OpenID_connect#Session_handling
|
||||
renewAfter: fromNow('15 minutes'),
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-disable consistent-return */
|
||||
export async function renew({ userSession, authController }) {
|
||||
if (
|
||||
!userSession
|
||||
|| userSession.type !== 'oidc'
|
||||
|| userSession.oidcProvider !== 'mozilla-auth0'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((accept, reject) => webAuth.renewAuth({}, (err, authResult) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
} if (!authResult) {
|
||||
return reject(new Error('no authResult'));
|
||||
}
|
||||
authController.setUserSession(userSessionFromAuthResult(authResult));
|
||||
accept();
|
||||
}));
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import ClientOAuth2 from 'client-oauth2';
|
||||
import UserSession from './UserSession';
|
||||
import config from '../../config';
|
||||
|
||||
export const webAuth = new ClientOAuth2(config.OAuth2Options);
|
||||
|
||||
// Part 1 - Redirect the user on Taskcluster instance to start the OAuth2 flow
|
||||
export function redirectUser() {
|
||||
window.location.href = webAuth.code.getUri();
|
||||
}
|
||||
|
||||
// Part 2 - Exchange Oauth code for Taskcluster credentials
|
||||
export async function userSessionFromCode(url) {
|
||||
// Get Oauth access token
|
||||
const user = await webAuth.code.getToken(url);
|
||||
|
||||
// Exchange that access token for some Taskcluster credentials
|
||||
const request = user.sign({
|
||||
method: 'get',
|
||||
});
|
||||
const resp = await fetch(config.OAuth2Options.credentialsUri, request);
|
||||
const payload = await resp.json();
|
||||
|
||||
// Finally build a new user session
|
||||
return UserSession.fromCredentials(payload.credentials);
|
||||
}
|
||||
|
||||
/* eslint-disable consistent-return */
|
||||
export async function renew({ userSession }) {
|
||||
if (
|
||||
!userSession
|
||||
|| userSession.type !== 'oidc'
|
||||
|| userSession.oidcProvider !== 'mozilla-auth0'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((accept, reject) => webAuth.renewAuth({}, (err, authResult) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
} if (!authResult) {
|
||||
return reject(new Error('no authResult'));
|
||||
}
|
||||
// TODO: somehow renew ? Not possible i think with Oauth + TC
|
||||
accept();
|
||||
}));
|
||||
}
|
|
@ -1,21 +1,24 @@
|
|||
const loginCallbackRoute = '/callback';
|
||||
const PRODUCTION = !(process.env.NODE_ENV === 'production');
|
||||
export const TASKCLUSTER_ROOT_URL = PRODUCTION ? 'https://firefox-ci-tc.services.mozilla.com' : 'https://stage.taskcluster.nonprod.cloudops.mozgcp.net';
|
||||
|
||||
const config = {
|
||||
redirectRoute: loginCallbackRoute,
|
||||
artifactRoute: 'project.relman.testing.bugzilla-dashboard.latest',
|
||||
artifactRoute: 'project.relman.production.bugzilla-dashboard.latest',
|
||||
taskclusterSecrets: {
|
||||
orgData: 'project/bugzilla-management-dashboard/realOrg',
|
||||
},
|
||||
auth0Options: {
|
||||
domain: process.env.ALTERNATIVE_AUTH ? 'mozilla-frontend-infra.auth0.com' : 'auth.mozilla.auth0.com',
|
||||
clientID: process.env.ALTERNATIVE_AUTH ? 'nWIQUJ5lOiyYHgK4Jm5nPs5hM6JUizwt' : 'DGloMN2BXb0AC7lF5eRyOe1GXweqBAiI',
|
||||
redirectUri: new URL(loginCallbackRoute, window.location).href,
|
||||
scope: 'taskcluster-credentials full-user-credentials openid profile email',
|
||||
audience: process.env.ALTERNATIVE_AUTH ? '' : 'login.taskcluster.net',
|
||||
responseType: 'token id_token',
|
||||
OAuth2Options: {
|
||||
clientId: PRODUCTION ? 'bugzilla-dashboard-production' : 'bugzilla-dashboard-localdev',
|
||||
scopes: ['queue:get-artifact:project/relman/bugzilla-dashboard/*'],
|
||||
authorizationUri: `${TASKCLUSTER_ROOT_URL}/login/oauth/authorize`,
|
||||
accessTokenUri: `${TASKCLUSTER_ROOT_URL}/login/oauth/token`,
|
||||
credentialsUri: `${TASKCLUSTER_ROOT_URL}/login/oauth/credentials`,
|
||||
redirectUri: PRODUCTION ? 'https://bugzilla-management-dashboard.netlify.com' : 'http://localhost:5000',
|
||||
whitelisted: true,
|
||||
responseType: 'code',
|
||||
maxExpires: '15 minutes',
|
||||
},
|
||||
productComponentMetrics: 'private/bugzilla-dashboard/product_component_data.json.gz',
|
||||
reporteesMetrics: 'private/bugzilla-dashboard/reportee_data.json.gz',
|
||||
productComponentMetrics: 'project/relman/bugzilla-dashboard/product_component_data.json.gz',
|
||||
reporteesMetrics: 'project/relman/bugzilla-dashboard/reportee_data.json.gz',
|
||||
};
|
||||
|
||||
export const REPORTEES_CONFIG = {
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
|
||||
import { webAuth, userSessionFromAuthResult } from '../../components/auth/auth0';
|
||||
|
||||
export default class Auth0Login extends React.PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.shape({}).isRequired,
|
||||
setUserSession: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
componentDidMount() {
|
||||
const { history, setUserSession } = this.props;
|
||||
|
||||
if (!window.location.hash) {
|
||||
webAuth.authorize();
|
||||
} else if (window !== window.top) {
|
||||
// for silent renewal, auth0-js opens this page in an iframe, and expects
|
||||
// a postMessage back, and that's it.
|
||||
window.parent.postMessage(window.location.hash, window.origin);
|
||||
} else {
|
||||
webAuth.parseHash(window.location.hash, (loginError, authResult) => {
|
||||
if (loginError) {
|
||||
this.setState({ loginError });
|
||||
} else {
|
||||
setUserSession(userSessionFromAuthResult(authResult));
|
||||
if (window.opener) {
|
||||
window.close();
|
||||
} else {
|
||||
history.push('/');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loginError } = this.state;
|
||||
if (loginError) {
|
||||
return <ErrorPanel error={loginError} />;
|
||||
}
|
||||
|
||||
if (window.location.hash) {
|
||||
return <p>Logging in..</p>;
|
||||
}
|
||||
|
||||
return <p>Redirecting..</p>;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ import { withStyles } from '@material-ui/core/styles';
|
|||
import Button from '@material-ui/core/Button';
|
||||
|
||||
import AuthContext from '../../components/auth/AuthContext';
|
||||
import config from '../../config';
|
||||
import ProfileMenu from '../ProfileMenu';
|
||||
|
||||
const styles = theme => ({
|
||||
|
@ -21,7 +20,7 @@ class CredentialsMenu extends React.PureComponent {
|
|||
};
|
||||
|
||||
static handleLoginRequest() {
|
||||
const loginView = new URL(config.redirectRoute, window.location);
|
||||
const loginView = new URL('/login', window.location);
|
||||
window.open(loginView, '_blank');
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
|
||||
import { redirectUser } from '../../components/auth/oauth2';
|
||||
|
||||
export default class OAuth2Login extends React.PureComponent {
|
||||
state = {};
|
||||
|
||||
componentDidMount() {
|
||||
if (!window.location.hash) {
|
||||
// Start login flow
|
||||
redirectUser();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loginError } = this.state;
|
||||
if (loginError) {
|
||||
return <ErrorPanel error={loginError} />;
|
||||
}
|
||||
|
||||
if (window.location.hash) {
|
||||
return <p>Logging in..</p>;
|
||||
}
|
||||
|
||||
return <p>Redirecting..</p>;
|
||||
}
|
||||
}
|
108
yarn.lock
108
yarn.lock
|
@ -1095,6 +1095,11 @@
|
|||
dependencies:
|
||||
any-observable "^0.3.0"
|
||||
|
||||
"@servie/events@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@servie/events/-/events-1.0.0.tgz#8258684b52d418ab7b86533e861186638ecc5dc1"
|
||||
integrity sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw==
|
||||
|
||||
"@types/babel__core@^7.1.0":
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.1.tgz#ce9a9e5d92b7031421e1d0d74ae59f572ba48be6"
|
||||
|
@ -1190,6 +1195,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
|
||||
|
||||
"@types/tough-cookie@^2.3.5":
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5"
|
||||
integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==
|
||||
|
||||
"@types/yargs@^12.0.2", "@types/yargs@^12.0.9":
|
||||
version "12.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
|
||||
|
@ -2051,6 +2061,11 @@ builtin-status-codes@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
||||
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
|
||||
|
||||
byte-length@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/byte-length/-/byte-length-1.0.2.tgz#ba5a5909240b0121c079b7f7b15248d6f08223cc"
|
||||
integrity sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==
|
||||
|
||||
bytes@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
|
||||
|
@ -2326,6 +2341,14 @@ cli-width@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
||||
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
|
||||
|
||||
client-oauth2@^4.2.5:
|
||||
version "4.2.5"
|
||||
resolved "https://registry.yarnpkg.com/client-oauth2/-/client-oauth2-4.2.5.tgz#cee9499ef0acc84ee545a76a8a51942ddf26f473"
|
||||
integrity sha512-GAhVLveAbBkwcfEH/d5lTW9eCgcPR3Up93cx7v4qWTdLCa4O0m3ykNNn4aAVeWOiHfWL5skO+3u0F/gfAxZuPQ==
|
||||
dependencies:
|
||||
popsicle "12.0.4"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
cliui@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
|
||||
|
@ -5923,6 +5946,18 @@ make-dir@^2.0.0, make-dir@^2.1.0:
|
|||
pify "^4.0.1"
|
||||
semver "^5.6.0"
|
||||
|
||||
make-error-cause@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-2.3.0.tgz#ecd11875971e506d510e93d37796e5b83f46d6f9"
|
||||
integrity sha512-etgt+n4LlOkGSJbBTV9VROHA5R7ekIPS4vfh+bCAoJgRrJWdqJCBbpS3osRJ/HrT7R68MzMiY3L3sDJ/Fd8aBg==
|
||||
dependencies:
|
||||
make-error "^1.3.5"
|
||||
|
||||
make-error@^1.3.5:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
|
||||
integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
|
||||
|
||||
makeerror@1.0.x:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
|
||||
|
@ -7061,6 +7096,57 @@ popper.js@^1.14.1:
|
|||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
|
||||
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
|
||||
|
||||
popsicle-content-encoding@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/popsicle-content-encoding/-/popsicle-content-encoding-1.0.0.tgz#2ab419083fee0387bf6e64d21b1a9af560795adb"
|
||||
integrity sha512-4Df+vTfM8wCCJVTzPujiI6eOl3SiWQkcZg0AMrOkD1enMXsF3glIkFUZGvour1Sj7jOWCsNSEhBxpbbhclHhzw==
|
||||
|
||||
popsicle-cookie-jar@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/popsicle-cookie-jar/-/popsicle-cookie-jar-1.0.0.tgz#9e8c89be7182b31f7ce0e66dad465ae475d8f47c"
|
||||
integrity sha512-vrlOGvNVELko0+J8NpGC5lHWDGrk8LQJq9nwAMIVEVBfN1Lib3BLxAaLRGDTuUnvl45j5N9dT2H85PULz6IjjQ==
|
||||
dependencies:
|
||||
"@types/tough-cookie" "^2.3.5"
|
||||
tough-cookie "^3.0.1"
|
||||
|
||||
popsicle-redirects@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/popsicle-redirects/-/popsicle-redirects-1.1.0.tgz#2a5abb49a7ad49c02e90b24d4608dc0b8b23176a"
|
||||
integrity sha512-XCpzVjVk7tty+IJnSdqWevmOr1n8HNDhL86v7mZ6T1JIIf2KGybxUk9mm7ZFOhWMkGB0e8XkacHip7BV8AQWQA==
|
||||
|
||||
popsicle-transport-http@^1.0.0:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/popsicle-transport-http/-/popsicle-transport-http-1.0.6.tgz#b73a65426f7ef9d0bfedd98673b84cd92e061bdd"
|
||||
integrity sha512-J/d1MhlqgaDro9xWe31RCNFBlUs3kG52rl7YNKYZdF8nllgGtXwhfcLlzwNJOW/M+nPOyxFvqOZIi6Qq599Hlw==
|
||||
dependencies:
|
||||
make-error-cause "^2.2.0"
|
||||
pump "^3.0.0"
|
||||
|
||||
popsicle-transport-xhr@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/popsicle-transport-xhr/-/popsicle-transport-xhr-1.0.2.tgz#aa4b7ab74d37f880cf857622cbbaf5ead3e43cb2"
|
||||
integrity sha512-v9eAJnj1tydT4VmDdyKFE1z/+oL01vB7AS3LfSFMAYv33dzqlxtbApKALcYWBQotIqw3FoIqd2FiDR6qJsOxtA==
|
||||
|
||||
popsicle-user-agent@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/popsicle-user-agent/-/popsicle-user-agent-1.0.0.tgz#976af355b605966168733c4e03ad1e4f783f5d48"
|
||||
integrity sha512-epKaq3TTfTzXcxBxjpoKYMcTTcAX8Rykus6QZu77XNhJuRHSRxMd+JJrbX/3PFI0opFGSN0BabbAYCbGxbu0mA==
|
||||
|
||||
popsicle@12.0.4:
|
||||
version "12.0.4"
|
||||
resolved "https://registry.yarnpkg.com/popsicle/-/popsicle-12.0.4.tgz#297adb1132a79fdbc54ca902645811b177f6234f"
|
||||
integrity sha512-UuxhAFa4RXBecC6ZK24sKra/9va1bTxnb3CQpFsm+VBW72sl+UtTAmZv7LZTvvDNnGusAqisN+a6xSN9xSQzZA==
|
||||
dependencies:
|
||||
popsicle-content-encoding "^1.0.0"
|
||||
popsicle-cookie-jar "^1.0.0"
|
||||
popsicle-redirects "^1.0.0"
|
||||
popsicle-transport-http "^1.0.0"
|
||||
popsicle-transport-xhr "^1.0.0"
|
||||
popsicle-user-agent "^1.0.0"
|
||||
servie "^4.0.6"
|
||||
throwback "^4.1.0"
|
||||
tough-cookie "^3.0.1"
|
||||
|
||||
portfinder@^1.0.21:
|
||||
version "1.0.21"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.21.tgz#60e1397b95ac170749db70034ece306b9a27e324"
|
||||
|
@ -8044,6 +8130,14 @@ serve-static@1.14.1:
|
|||
parseurl "~1.3.3"
|
||||
send "0.17.1"
|
||||
|
||||
servie@^4.0.6:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/servie/-/servie-4.3.2.tgz#7168140d62cb9476cb8b184fc8ceda24d5154e7e"
|
||||
integrity sha512-1NpFf3LjkDDq4IIuBqtqHfSdPWhXpuyWwuBdwbifZjWSxQd8rCWz5W9AluxNvWfteM1qQ26puODIzWljvBJc5A==
|
||||
dependencies:
|
||||
"@servie/events" "^1.0.0"
|
||||
byte-length "^1.0.2"
|
||||
|
||||
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
|
@ -8715,6 +8809,11 @@ through@^2.3.6, through@~2.3.6:
|
|||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
|
||||
throwback@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/throwback/-/throwback-4.1.0.tgz#421aac7ba9eff473105385ac4a2b0130d4b0a59c"
|
||||
integrity sha512-dLFe8bU8SeH0xeqeKL7BNo8XoPC/o91nz9/ooeplZPiso+DZukhoyZcSz9TFnUNScm+cA9qjU1m1853M6sPOng==
|
||||
|
||||
thunky@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826"
|
||||
|
@ -8802,6 +8901,15 @@ tough-cookie@^2.3.3, tough-cookie@^2.3.4:
|
|||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tough-cookie@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
|
||||
integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
|
||||
dependencies:
|
||||
ip-regex "^2.1.0"
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tough-cookie@~2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
|
||||
|
|
Загрузка…
Ссылка в новой задаче