mirror of
https://github.com/logto-io/logto.git
synced 2025-03-17 22:31:28 -05:00
feat(ui): add social web login flow (#531)
* feat(ui): add social web login flow add social web login flow * fix(ui): cr fix code review fix * fix(ui): fix typo fix typo * refactor(ui): social api renaming social api renaming
This commit is contained in:
parent
1f6f0a3796
commit
7dba17b867
20 changed files with 391 additions and 23 deletions
|
@ -22,6 +22,7 @@
|
|||
"classnames": "^2.3.1",
|
||||
"i18next": "^21.6.11",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"js-base64": "^3.7.2",
|
||||
"ky": "^0.29.0",
|
||||
"libphonenumber-js": "^1.9.49",
|
||||
"react": "^17.0.2",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"@jest/types": "^27.5.1",
|
||||
"@parcel/core": "^2.3.2",
|
||||
"@parcel/transformer-sass": "^2.3.2",
|
||||
"@peculiar/webcrypto": "^1.3.3",
|
||||
"@silverhand/eslint-config": "^0.10.2",
|
||||
"@silverhand/eslint-config-react": "^0.10.3",
|
||||
"@silverhand/ts-config": "^0.10.2",
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Route, Routes, BrowserRouter } from 'react-router-dom';
|
|||
import AppContent from './components/AppContent';
|
||||
import useTheme from './hooks/use-theme';
|
||||
import initI18n from './i18n/init';
|
||||
import Callback from './pages/Callback';
|
||||
import Consent from './pages/Consent';
|
||||
import Passcode from './pages/Passcode';
|
||||
import Register from './pages/Register';
|
||||
|
@ -27,6 +28,7 @@ const App = () => {
|
|||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/register/:channel" element={<Register />} />
|
||||
<Route path="/:type/:channel/passcode-validation" element={<Passcode />} />
|
||||
<Route path="/callback/:connector" element={<Callback />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AppContent>
|
||||
|
|
|
@ -1,2 +1,29 @@
|
|||
export const appLogo = 'https://avatars.githubusercontent.com/u/88327661?s=200&v=4';
|
||||
export const appHeadline = 'Build user identity in a modern way';
|
||||
export const socialConnectors = [
|
||||
{
|
||||
id: 'github',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: 'GitHub',
|
||||
},
|
||||
{
|
||||
id: 'alipay',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: 'alipay',
|
||||
},
|
||||
{
|
||||
id: 'wechat',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: 'wechat',
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: 'google',
|
||||
},
|
||||
{
|
||||
id: 'facebook',
|
||||
logo: 'https://user-images.githubusercontent.com/5717882/156983224-7ea0296b-38fa-419d-9515-67e8a9612e09.png',
|
||||
name: 'Meta',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -15,7 +15,12 @@ import {
|
|||
verifyEmailPasscode,
|
||||
verifyPhonePasscode,
|
||||
} from './sign-in';
|
||||
import { signInWithSocial, signInToSoical, bindSocialAccount, registerWithSocial } from './social';
|
||||
import {
|
||||
invokeSocialSignIn,
|
||||
signInWithSoical,
|
||||
bindSocialAccount,
|
||||
registerWithSocial,
|
||||
} from './social';
|
||||
|
||||
jest.mock('ky', () => ({
|
||||
post: jest.fn(() => ({
|
||||
|
@ -137,8 +142,8 @@ describe('api', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('signInWithSocial', async () => {
|
||||
await signInWithSocial('connectorId', 'state', 'redirectUri');
|
||||
it('invokeSocialSignIn', async () => {
|
||||
await invokeSocialSignIn('connectorId', 'state', 'redirectUri');
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/social', {
|
||||
json: {
|
||||
connectorId: 'connectorId',
|
||||
|
@ -148,14 +153,14 @@ describe('api', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('signInToSoical', async () => {
|
||||
it('signInWithSoical', async () => {
|
||||
const parameters = {
|
||||
connectorId: 'connectorId',
|
||||
state: 'state',
|
||||
redirectUri: 'redirectUri',
|
||||
code: 'code',
|
||||
};
|
||||
await signInToSoical(parameters);
|
||||
await signInWithSoical(parameters);
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/social', {
|
||||
json: parameters,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import ky from 'ky';
|
||||
|
||||
export const signInWithSocial = async (connectorId: string, state: string, redirectUri: string) => {
|
||||
export const invokeSocialSignIn = async (
|
||||
connectorId: string,
|
||||
state: string,
|
||||
redirectUri: string
|
||||
) => {
|
||||
type Response = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
@ -16,7 +20,7 @@ export const signInWithSocial = async (connectorId: string, state: string, redir
|
|||
.json<Response>();
|
||||
};
|
||||
|
||||
export const signInToSoical = async (parameters: {
|
||||
export const signInWithSoical = async (parameters: {
|
||||
connectorId: string;
|
||||
state: string;
|
||||
redirectUri: string;
|
||||
|
|
4
packages/ui/src/assets/icons/more-social-icon.svg
Normal file
4
packages/ui/src/assets/icons/more-social-icon.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg id="more" width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24" r="19" stroke="#AEAEAE" stroke-width="2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 24C17 25.1046 16.1046 26 15 26C13.8954 26 13 25.1046 13 24C13 22.8954 13.8954 22 15 22C16.1046 22 17 22.8954 17 24ZM26 24C26 25.1046 25.1046 26 24 26C22.8954 26 22 25.1046 22 24C22 22.8954 22.8954 22 24 22C25.1046 22 26 22.8954 26 24ZM33 26C34.1046 26 35 25.1046 35 24C35 22.8954 34.1046 22 33 22C31.8954 22 31 22.8954 31 24C31 25.1046 31.8954 26 33 26Z" fill="#AEAEAE"/>
|
||||
</svg>
|
After Width: | Height: | Size: 609 B |
17
packages/ui/src/components/Button/MoreButton.tsx
Normal file
17
packages/ui/src/components/Button/MoreButton.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './SocialIconButton.module.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const MoreButton = ({ className, onClick }: Props) => {
|
||||
return (
|
||||
<button className={classNames(styles.socialButton, styles.more, className)} onClick={onClick} />
|
||||
);
|
||||
};
|
||||
|
||||
export default MoreButton;
|
|
@ -0,0 +1,21 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
|
||||
.socialButton {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
@include _.flex-column;
|
||||
background: var(--color-secondary-background-active);
|
||||
border: none;
|
||||
|
||||
&.more {
|
||||
background: url('../../assets/icons/more-social-icon.svg') no-repeat center;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
@include _.image-align-center;
|
||||
}
|
23
packages/ui/src/components/Button/SocialIconButton.tsx
Normal file
23
packages/ui/src/components/Button/SocialIconButton.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ConnectorMetadata } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './SocialIconButton.module.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
connector: Pick<ConnectorMetadata, 'id' | 'logo'>;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const SocialIconButton = ({ className, connector, onClick }: Props) => {
|
||||
const { id, logo } = connector;
|
||||
|
||||
return (
|
||||
<button className={classNames(styles.socialButton, className)} onClick={onClick}>
|
||||
{logo && <img src={logo} alt={id} className={styles.icon} />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialIconButton;
|
|
@ -13,20 +13,4 @@
|
|||
@include _.image-align-center;
|
||||
margin-right: _.unit(4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--color-secondary-background-active);
|
||||
border: _.border(var(--color-border-active));
|
||||
color: var(--color-font-primary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--color-secondary-background-disabled);
|
||||
border: _.border(var(--color-border-disabled));
|
||||
color: var(--color-font-secondary-disabled);
|
||||
|
||||
.icon {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { socialConnectors } from '@/__mocks__/logto';
|
||||
|
||||
import SecondarySocialSignIn from './SecondarySocialSignIn';
|
||||
|
||||
describe('SecondarySocialSignIn', () => {
|
||||
it('less than four connectors', () => {
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<SecondarySocialSignIn connectors={socialConnectors.slice(0, 3)} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(container.querySelectorAll('button')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('more than four connectors', () => {
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<SecondarySocialSignIn connectors={socialConnectors} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(container.querySelectorAll('button')).toHaveLength(4);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import { ConnectorMetadata } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import MoreButton from '@/components/Button/MoreButton';
|
||||
import SocialIconButton from '@/components/Button/SocialIconButton';
|
||||
import useSocial from '@/hooks/use-social-connector';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
connectors: Array<Pick<ConnectorMetadata, 'id' | 'logo'>>;
|
||||
};
|
||||
|
||||
const SecondarySocialSignIn = ({ className, connectors }: Props) => {
|
||||
const { invokeSocialSignIn } = useSocial();
|
||||
const sampled = connectors.length > 4;
|
||||
|
||||
const sampledConnectors = useMemo(() => {
|
||||
// TODO: filter with native returned
|
||||
|
||||
if (sampled) {
|
||||
return connectors.slice(0, 3);
|
||||
}
|
||||
|
||||
return connectors;
|
||||
}, [connectors, sampled]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.socialIconList, className)}>
|
||||
{sampledConnectors.map((connector) => (
|
||||
<SocialIconButton
|
||||
key={connector.id}
|
||||
className={styles.socialButton}
|
||||
connector={connector}
|
||||
onClick={() => {
|
||||
void invokeSocialSignIn(connector.id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{sampled && <MoreButton className={styles.socialButton} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondarySocialSignIn;
|
16
packages/ui/src/containers/SocialSignIn/index.module.scss
Normal file
16
packages/ui/src/containers/SocialSignIn/index.module.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.socialIconList {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
@include _.flex-row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.socialButton {
|
||||
margin-right: _.unit(10);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
1
packages/ui/src/containers/SocialSignIn/index.ts
Normal file
1
packages/ui/src/containers/SocialSignIn/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as SecondarySocialSignIn } from './SecondarySocialSignIn';
|
87
packages/ui/src/hooks/use-social-connector.ts
Normal file
87
packages/ui/src/hooks/use-social-connector.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { useEffect, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { invokeSocialSignIn, signInWithSoical } from '@/apis/social';
|
||||
import { generateRandomString } from '@/utils';
|
||||
|
||||
import useApi from './use-api';
|
||||
|
||||
const storageKeyPrefix = 'social_auth_state';
|
||||
const webPlatformPrefix = 'web';
|
||||
const mobilePlatformPrefix = 'mobile';
|
||||
|
||||
const isMobileWebview = () => {
|
||||
// TODO: read from native sdk embedded params
|
||||
return true;
|
||||
};
|
||||
|
||||
const useSocial = () => {
|
||||
const { result: invokeSocialSignInResult, run: asyncSignInWithSocial } =
|
||||
useApi(invokeSocialSignIn);
|
||||
const { result: signInToSocialResult, run: asyncSignInWithSoical } = useApi(signInWithSoical);
|
||||
|
||||
const { search } = useLocation();
|
||||
|
||||
const validateState = useCallback((state: string, connectorId: string) => {
|
||||
if (state.startsWith(mobilePlatformPrefix)) {
|
||||
return true; // Not able to validate the state source from the native call stack
|
||||
}
|
||||
|
||||
const stateStorage = sessionStorage.getItem(`${storageKeyPrefix}:${connectorId}`);
|
||||
|
||||
return stateStorage === state;
|
||||
}, []);
|
||||
|
||||
const signInWithSocialHandler = useCallback(
|
||||
(connector?: string) => {
|
||||
const uriParameters = new URLSearchParams(search);
|
||||
const state = uriParameters.get('state');
|
||||
const code = uriParameters.get('code');
|
||||
|
||||
if (!state || !code || !connector) {
|
||||
// TODO: error message
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateState(state, connector)) {
|
||||
// TODO: error message
|
||||
return;
|
||||
}
|
||||
|
||||
void asyncSignInWithSoical({ connectorId: connector, state, code, redirectUri: 'TODO' });
|
||||
},
|
||||
[asyncSignInWithSoical, search, validateState]
|
||||
);
|
||||
|
||||
const invokeSocialSignInHandler = useCallback(
|
||||
async (connectorId: string) => {
|
||||
const state = `${
|
||||
isMobileWebview() ? mobilePlatformPrefix : webPlatformPrefix
|
||||
}_${generateRandomString()}`;
|
||||
const { origin } = window.location;
|
||||
sessionStorage.setItem(`${storageKeyPrefix}:${connectorId}`, state);
|
||||
|
||||
return asyncSignInWithSocial(connectorId, state, `${origin}/callback/${connectorId}`);
|
||||
},
|
||||
[asyncSignInWithSocial]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (invokeSocialSignInResult?.redirectTo) {
|
||||
window.location.assign(invokeSocialSignInResult.redirectTo);
|
||||
}
|
||||
}, [invokeSocialSignInResult]);
|
||||
|
||||
useEffect(() => {
|
||||
if (signInToSocialResult?.redirectTo) {
|
||||
window.location.assign(signInToSocialResult.redirectTo);
|
||||
}
|
||||
}, [signInToSocialResult]);
|
||||
|
||||
return {
|
||||
invokeSocialSignIn: invokeSocialSignInHandler,
|
||||
signInWithSocial: signInWithSocialHandler,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSocial;
|
|
@ -1,3 +1,5 @@
|
|||
import { Crypto } from '@peculiar/webcrypto';
|
||||
|
||||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
|
@ -15,6 +17,9 @@ Object.defineProperty(window, 'matchMedia', {
|
|||
})),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
global.crypto = new Crypto();
|
||||
|
||||
const translation = (key: string) => key;
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
|
|
21
packages/ui/src/pages/Callback/index.tsx
Normal file
21
packages/ui/src/pages/Callback/index.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import useSocial from '@/hooks/use-social-connector';
|
||||
|
||||
type Props = {
|
||||
connector?: string;
|
||||
};
|
||||
|
||||
const Callback = () => {
|
||||
const { connector } = useParams<Props>();
|
||||
const { signInWithSocial } = useSocial();
|
||||
|
||||
useEffect(() => {
|
||||
signInWithSocial(connector);
|
||||
}, [signInWithSocial, connector]);
|
||||
|
||||
return <div>{connector} loading...</div>;
|
||||
};
|
||||
|
||||
export default Callback;
|
8
packages/ui/src/utils/index.test.ts
Normal file
8
packages/ui/src/utils/index.test.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { generateRandomString } from '.';
|
||||
|
||||
describe('util methods', () => {
|
||||
it('generateRandomString', () => {
|
||||
const random = generateRandomString();
|
||||
expect(random).not.toBeNull();
|
||||
});
|
||||
});
|
4
packages/ui/src/utils/index.ts
Normal file
4
packages/ui/src/utils/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { fromUint8Array } from 'js-base64';
|
||||
|
||||
export const generateRandomString = (length = 16) =>
|
||||
fromUint8Array(crypto.getRandomValues(new Uint8Array(length)), true);
|
63
pnpm-lock.yaml
generated
63
pnpm-lock.yaml
generated
|
@ -353,6 +353,7 @@ importers:
|
|||
'@logto/schemas': ^0.1.0
|
||||
'@parcel/core': ^2.3.2
|
||||
'@parcel/transformer-sass': ^2.3.2
|
||||
'@peculiar/webcrypto': ^1.3.3
|
||||
'@silverhand/eslint-config': ^0.10.2
|
||||
'@silverhand/eslint-config-react': ^0.10.3
|
||||
'@silverhand/essentials': ^1.1.7
|
||||
|
@ -371,6 +372,7 @@ importers:
|
|||
identity-obj-proxy: ^3.0.0
|
||||
jest: ^27.5.1
|
||||
jest-transform-stub: ^2.0.0
|
||||
js-base64: ^3.7.2
|
||||
ky: ^0.29.0
|
||||
libphonenumber-js: ^1.9.49
|
||||
lint-staged: ^11.1.1
|
||||
|
@ -396,6 +398,7 @@ importers:
|
|||
classnames: 2.3.1
|
||||
i18next: 21.6.11
|
||||
i18next-browser-languagedetector: 6.1.3
|
||||
js-base64: 3.7.2
|
||||
ky: 0.29.0
|
||||
libphonenumber-js: 1.9.49
|
||||
react: 17.0.2
|
||||
|
@ -410,6 +413,7 @@ importers:
|
|||
'@jest/types': 27.5.1
|
||||
'@parcel/core': 2.3.2
|
||||
'@parcel/transformer-sass': 2.3.2_@parcel+core@2.3.2
|
||||
'@peculiar/webcrypto': 1.3.3
|
||||
'@silverhand/eslint-config': 0.10.2_3a533fa6cc3da0cf8525ef55d41c4384
|
||||
'@silverhand/eslint-config-react': 0.10.3_75f27ebc3d08f23e7ca8c3f63c7fa502
|
||||
'@silverhand/ts-config': 0.10.2_typescript@4.6.2
|
||||
|
@ -5103,6 +5107,33 @@ packages:
|
|||
nullthrows: 1.1.1
|
||||
dev: true
|
||||
|
||||
/@peculiar/asn1-schema/2.1.0:
|
||||
resolution: {integrity: sha512-D6g4C5YRKC/iPujMAOXuZ7YGdaoMx8GsvWzfVSyx2LYeL38ECOKNywlYAuwbqQvON64lgsYdAujWQPX8hhoBLw==}
|
||||
dependencies:
|
||||
'@types/asn1js': 2.0.2
|
||||
asn1js: 2.3.2
|
||||
pvtsutils: 1.2.2
|
||||
tslib: 2.3.1
|
||||
dev: true
|
||||
|
||||
/@peculiar/json-schema/1.1.12:
|
||||
resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dependencies:
|
||||
tslib: 2.3.1
|
||||
dev: true
|
||||
|
||||
/@peculiar/webcrypto/1.3.3:
|
||||
resolution: {integrity: sha512-+jkp16Hp18HkphJlMtqsQKjyDWJBh0AhDuoB+vVakuIRbkBdaFb7v26Ldm25altjiYhCyQnR5NChHxwSTvbXJw==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.1.0
|
||||
'@peculiar/json-schema': 1.1.12
|
||||
pvtsutils: 1.2.2
|
||||
tslib: 2.3.1
|
||||
webcrypto-core: 1.7.3
|
||||
dev: true
|
||||
|
||||
/@polka/url/1.0.0-next.21:
|
||||
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
|
||||
dev: false
|
||||
|
@ -5574,6 +5605,10 @@ packages:
|
|||
resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==}
|
||||
dev: true
|
||||
|
||||
/@types/asn1js/2.0.2:
|
||||
resolution: {integrity: sha512-t4YHCgtD+ERvH0FyxvNlYwJ2ezhqw7t+Ygh4urQ7dJER8i185JPv6oIM3ey5YQmGN6Zp9EMbpohkjZi9t3UxwA==}
|
||||
dev: true
|
||||
|
||||
/@types/babel__core/7.1.17:
|
||||
resolution: {integrity: sha512-6zzkezS9QEIL8yCBvXWxPTJPNuMeECJVxSOhxNY/jfq9LxOTHivaYTqr37n9LknWWRTIkzqH2UilS5QFvfa90A==}
|
||||
dependencies:
|
||||
|
@ -6726,6 +6761,13 @@ packages:
|
|||
safer-buffer: 2.1.2
|
||||
dev: true
|
||||
|
||||
/asn1js/2.3.2:
|
||||
resolution: {integrity: sha512-IYzujqcOk7fHaePpTyvD3KPAA0AjT3qZlaQAw76zmPPAV/XTjhO+tbHjbFbIQZIhw+fk9wCSfb0Z6K+JHe8Q2g==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dependencies:
|
||||
pvutils: 1.1.3
|
||||
dev: true
|
||||
|
||||
/assert-plus/1.0.0:
|
||||
resolution: {integrity: sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=}
|
||||
engines: {node: '>=0.8'}
|
||||
|
@ -15792,6 +15834,17 @@ packages:
|
|||
resolution: {integrity: sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4=}
|
||||
dev: false
|
||||
|
||||
/pvtsutils/1.2.2:
|
||||
resolution: {integrity: sha512-OALo5ZEdqiI127i64+CXwkCOyFHUA+tCQgaUO/MvRDFXWPr53f2sx28ECNztUEzuyu5xvuuD1EB/szg9mwJoGA==}
|
||||
dependencies:
|
||||
tslib: 2.3.1
|
||||
dev: true
|
||||
|
||||
/pvutils/1.1.3:
|
||||
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: true
|
||||
|
||||
/q/1.5.1:
|
||||
resolution: {integrity: sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=}
|
||||
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
|
||||
|
@ -19158,6 +19211,16 @@ packages:
|
|||
resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==}
|
||||
dev: false
|
||||
|
||||
/webcrypto-core/1.7.3:
|
||||
resolution: {integrity: sha512-8TnMtwwC/hQOyvElAOJ26lJKGgcErUG02KnKS1+QhjV4mDvQetVWU1EUEeLF8ICOrdc42+GypocyBJKRqo2kQg==}
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.1.0
|
||||
'@peculiar/json-schema': 1.1.12
|
||||
asn1js: 2.3.2
|
||||
pvtsutils: 1.2.2
|
||||
tslib: 2.3.1
|
||||
dev: true
|
||||
|
||||
/webidl-conversions/3.0.1:
|
||||
resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue