diff options
-rw-r--r-- | packages/anastasis-webui/package.json | 1 | ||||
-rw-r--r-- | packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts | 185 | ||||
-rw-r--r-- | packages/anastasis-webui/src/routes/home/index.tsx | 518 | ||||
-rw-r--r-- | packages/anastasis-webui/src/routes/home/style.css | 25 | ||||
-rw-r--r-- | pnpm-lock.yaml | 51 |
5 files changed, 692 insertions, 88 deletions
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json index ddbd9ef20..fe332be03 100644 --- a/packages/anastasis-webui/package.json +++ b/packages/anastasis-webui/package.json @@ -21,6 +21,7 @@ ] }, "dependencies": { + "@gnu-taler/taler-util": "workspace:^0.8.3", "preact": "^10.3.1", "preact-render-to-string": "^5.1.4", "preact-router": "^3.2.1" diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts index 30bab96d1..d578d1418 100644 --- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts +++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts @@ -1,6 +1,58 @@ import { useState } from "preact/hooks"; -type ReducerState = any; +export type ReducerState = + | ReducerStateBackup + | ReducerStateRecovery + | ReducerStateError; + +export interface ReducerStateBackup { + recovery_state: undefined; + backup_state: BackupStates; + code: undefined; + continents: any; + countries: any; + authentication_providers: any; + authentication_methods?: AuthMethod[]; + required_attributes: any; + secret_name?: string; + policies?: { + methods: { + authentication_method: number; + provider: string; + }[]; + }[]; + success_details: { + [provider_url: string]: { + policy_version: number; + }; + }; + payments?: string[]; + policy_payment_requests?: { + payto: string; + provider: string; + }[]; +} + +export interface AuthMethod { + type: string; + instructions: string; + challenge: string; +} + +export interface ReducerStateRecovery { + backup_state: undefined; + recovery_state: RecoveryStates; + code: undefined; + + continents: any; + countries: any; +} + +export interface ReducerStateError { + backup_state: undefined; + recovery_state: undefined; + code: number; +} interface AnastasisState { reducerState: ReducerState | undefined; @@ -10,6 +62,13 @@ interface AnastasisState { export enum BackupStates { ContinentSelecting = "CONTINENT_SELECTING", CountrySelecting = "COUNTRY_SELECTING", + UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING", + AuthenticationsEditing = "AUTHENTICATIONS_EDITING", + PoliciesReviewing = "POLICIES_REVIEWING", + SecretEditing = "SECRET_EDITING", + TruthsPaying = "TRUTHS_PAYING", + PoliciesPaying = "POLICIES_PAYING", + BackupFinished = "BACKUP_FINISHED", } export enum RecoveryStates { @@ -49,20 +108,62 @@ async function reduceState( return resp.json(); } +export interface ReducerTransactionHandle { + transactionState: ReducerState; + transition(action: string, args: any): Promise<ReducerState>; +} + export interface AnastasisReducerApi { - currentReducerState: ReducerState; + currentReducerState: ReducerState | undefined; currentError: any; + dismissError: () => void; startBackup: () => void; startRecover: () => void; + reset: () => void; back: () => void; transition(action: string, args: any): void; + /** + * Run multiple reducer steps in a transaction without + * affecting the UI-visible transition state in-between. + */ + runTransaction(f: (h: ReducerTransactionHandle) => Promise<void>): void; +} + +function restoreState(): any { + let state: any; + try { + let s = localStorage.getItem("anastasisReducerState"); + if (s === "undefined") { + state = undefined; + } else if (s) { + console.log("restoring state from", s); + state = JSON.parse(s); + } + } catch (e) { + console.log(e); + } + return state ?? undefined; } export function useAnastasisReducer(): AnastasisReducerApi { - const [anastasisState, setAnastasisState] = useState<AnastasisState>({ - reducerState: undefined, - currentError: undefined, - }); + const [anastasisState, setAnastasisStateInternal] = useState<AnastasisState>( + () => ({ + reducerState: restoreState(), + currentError: undefined, + }), + ); + + const setAnastasisState = (newState: AnastasisState) => { + try { + localStorage.setItem( + "anastasisReducerState", + JSON.stringify(newState.reducerState), + ); + } catch (e) { + console.log(e); + } + setAnastasisStateInternal(newState); + }; async function doTransition(action: string, args: any) { console.log("reducing with", action, args); @@ -102,30 +203,74 @@ export function useAnastasisReducer(): AnastasisReducerApi { doTransition(action, args); }, back() { + const reducerState = anastasisState.reducerState; + if (!reducerState) { + return; + } if ( - anastasisState.reducerState.backup_state === - BackupStates.ContinentSelecting || - anastasisState.reducerState.recovery_state === - RecoveryStates.ContinentSelecting + reducerState.backup_state === BackupStates.ContinentSelecting || + reducerState.recovery_state === RecoveryStates.ContinentSelecting ) { setAnastasisState({ ...anastasisState, currentError: undefined, reducerState: undefined, }); - } else if ( - anastasisState.reducerState.backup_state === - BackupStates.CountrySelecting - ) { - doTransition("unselect_continent", {}); - } else if ( - anastasisState.reducerState.recovery_state === - RecoveryStates.CountrySelecting - ) { - doTransition("unselect_continent", {}); } else { doTransition("back", {}); } }, + dismissError() { + setAnastasisState({ ...anastasisState, currentError: undefined }); + }, + reset() { + setAnastasisState({ + ...anastasisState, + currentError: undefined, + reducerState: undefined, + }); + }, + runTransaction(f) { + async function run() { + const txHandle = new ReducerTxImpl(anastasisState.reducerState!); + try { + await f(txHandle); + } catch (e) { + console.log("exception during reducer transaction", e); + } + const s = txHandle.transactionState; + console.log("transaction finished, new state", s); + if (s.code !== undefined) { + setAnastasisState({ + ...anastasisState, + currentError: txHandle.transactionState, + }); + } else { + setAnastasisState({ + ...anastasisState, + reducerState: txHandle.transactionState, + currentError: undefined, + }); + } + } + run(); + }, }; } + +class ReducerTxImpl implements ReducerTransactionHandle { + constructor(public transactionState: ReducerState) {} + async transition(action: string, args: any): Promise<ReducerState> { + console.log("making transition in transaction", action); + this.transactionState = await reduceState( + this.transactionState, + action, + args, + ); + // Abort transaction as soon as we transition into an error state. + if (this.transactionState.code !== undefined) { + throw Error("transition resulted in error"); + } + return this.transactionState; + } +} diff --git a/packages/anastasis-webui/src/routes/home/index.tsx b/packages/anastasis-webui/src/routes/home/index.tsx index ee3399503..f61897682 100644 --- a/packages/anastasis-webui/src/routes/home/index.tsx +++ b/packages/anastasis-webui/src/routes/home/index.tsx @@ -1,80 +1,290 @@ +import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util"; import { FunctionalComponent, h } from "preact"; import { useState } from "preact/hooks"; import { AnastasisReducerApi, + AuthMethod, + BackupStates, + ReducerStateBackup, + ReducerStateRecovery, useAnastasisReducer, } from "../../hooks/use-anastasis-reducer"; import style from "./style.css"; +interface ContinentSelectionProps { + reducer: AnastasisReducerApi; + reducerState: ReducerStateBackup | ReducerStateRecovery; +} + +function isBackup(reducer: AnastasisReducerApi) { + return !!reducer.currentReducerState?.backup_state; +} + +function ContinentSelection(props: ContinentSelectionProps) { + const { reducer, reducerState } = props; + return ( + <div class={style.home}> + <h1>{isBackup(reducer) ? "Backup" : "Recovery"}: Select Continent</h1> + <ErrorBanner reducer={reducer} /> + <div> + {reducerState.continents.map((x: any) => { + const sel = (x: string) => + reducer.transition("select_continent", { continent: x }); + return ( + <button onClick={() => sel(x.name)} key={x.name}> + {x.name} + </button> + ); + })} + </div> + <div> + <button onClick={() => reducer.back()}>Back</button> + </div> + </div> + ); +} + +interface CountrySelectionProps { + reducer: AnastasisReducerApi; + reducerState: ReducerStateBackup | ReducerStateRecovery; +} + +function CountrySelection(props: CountrySelectionProps) { + const { reducer, reducerState } = props; + return ( + <div class={style.home}> + <h1>Backup: Select Country</h1> + <ErrorBanner reducer={reducer} /> + <div> + {reducerState.countries.map((x: any) => { + const sel = (x: any) => + reducer.transition("select_country", { + country_code: x.code, + currencies: [x.currency], + }); + return ( + <button onClick={() => sel(x)} key={x.name}> + {x.name} ({x.currency}) + </button> + ); + })} + </div> + <div> + <button onClick={() => reducer.back()}>Back</button> + </div> + </div> + ); +} + const Home: FunctionalComponent = () => { const reducer = useAnastasisReducer(); - if (!reducer.currentReducerState) { + const reducerState = reducer.currentReducerState; + if (!reducerState) { return ( <div class={style.home}> <h1>Home</h1> <p> - <button onClick={() => reducer.startBackup()}>Backup</button> - <button>Recover</button> + <button autoFocus onClick={() => reducer.startBackup()}> + Backup + </button> + <button onClick={() => reducer.startRecover()}>Recover</button> </p> </div> ); } console.log("state", reducer.currentReducerState); - if (reducer.currentReducerState.backup_state === "CONTINENT_SELECTING") { + + if (reducerState.backup_state === BackupStates.ContinentSelecting) { + return <ContinentSelection reducer={reducer} reducerState={reducerState} />; + } + if (reducerState.backup_state === BackupStates.CountrySelecting) { + return <CountrySelection reducer={reducer} reducerState={reducerState} />; + } + if (reducerState.backup_state === BackupStates.UserAttributesCollecting) { + return <AttributeEntry reducer={reducer} backupState={reducerState} />; + } + if (reducerState.backup_state === BackupStates.AuthenticationsEditing) { + return ( + <AuthenticationEditor backupState={reducerState} reducer={reducer} /> + ); + } + + if (reducerState.backup_state === BackupStates.PoliciesReviewing) { + const backupState: ReducerStateBackup = reducerState; + const authMethods = backupState.authentication_methods!; return ( <div class={style.home}> - <h1>Backup: Select Continent</h1> + <h1>Backup: Review Recovery Policies</h1> <ErrorBanner reducer={reducer} /> <div> - {reducer.currentReducerState.continents.map((x: any) => { - const sel = (x: string) => - reducer.transition("select_continent", { continent: x }); + {backupState.policies?.map((p, i) => { + const policyName = p.methods + .map((x) => authMethods[x.authentication_method].type) + .join(" + "); return ( - <button onClick={() => sel(x.name)} key={x.name}> - {x.name} - </button> + <div class={style.policy}> + <h3> + Policy #{i + 1}: {policyName} + </h3> + Required Authentications: + <ul> + {p.methods.map((x) => { + const m = authMethods[x.authentication_method]; + return ( + <li> + {m.type} ({m.instructions}) at provider {x.provider} + </li> + ); + })} + </ul> + <div> + <button + onClick={() => + reducer.transition("delete_policy", { policy_index: i }) + } + > + Delete Policy + </button> + </div> + </div> ); })} </div> <div> <button onClick={() => reducer.back()}>Back</button> + <button onClick={() => reducer.transition("next", {})}>Next</button> </div> </div> ); } - if (reducer.currentReducerState.backup_state === "COUNTRY_SELECTING") { + + if (reducerState.backup_state === BackupStates.SecretEditing) { + const [secretName, setSecretName] = useState(""); + const [secretValue, setSecretValue] = useState(""); + const secretNext = () => { + reducer.runTransaction(async (tx) => { + await tx.transition("enter_secret_name", { + name: secretName, + }); + await tx.transition("enter_secret", { + secret: { + value: "EDJP6WK5EG50", + mime: "text/plain", + }, + expiration: { + t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5, + }, + }); + await tx.transition("next", {}); + }); + }; return ( <div class={style.home}> - <h1>Backup: Select Continent</h1> + <h1>Backup: Provide secret</h1> <ErrorBanner reducer={reducer} /> <div> - {reducer.currentReducerState.countries.map((x: any) => { - const sel = (x: any) => - reducer.transition("select_country", { - country_code: x.code, - currencies: [x.currency], - }); + <label> + Secret name: <input type="text" /> + </label> + </div> + <div> + <label> + Secret value: <input type="text" /> + </label> + </div> + or: + <div> + <label> + File Upload: <input type="file" /> + </label> + </div> + <div> + <button onClick={() => reducer.back()}>Back</button> + <button onClick={() => secretNext()}>Next</button> + </div> + </div> + ); + } + + if (reducerState.backup_state === BackupStates.BackupFinished) { + const backupState: ReducerStateBackup = reducerState; + return ( + <div class={style.home}> + <h1>Backup finished</h1> + <p> + Your backup of secret "{backupState.secret_name ?? "??"}" was + successful. + </p> + <p>The backup is stored by the following providers:</p> + <ul> + {Object.keys(backupState.success_details).map((x, i) => { + const sd = backupState.success_details[x]; return ( - <button onClick={() => sel(x)} key={x.name}> - {x.name} ({x.currency}) - </button> + <li> + {x} (Policy version {sd.policy_version}) + </li> ); })} - </div> + </ul> + <button onClick={() => reducer.reset()}> + Start a new backup/recovery + </button> + </div> + ); + } + + if (reducerState.backup_state === BackupStates.TruthsPaying) { + const backupState: ReducerStateBackup = reducerState; + const payments = backupState.payments ?? []; + return ( + <div class={style.home}> + <h1>Backup: Authentication Storage Payments</h1> + <p> + Some of the providers require a payment to store the encrypted + authentication information. + </p> + <ul> + {payments.map((x) => { + return <li>{x}</li>; + })} + </ul> <div> <button onClick={() => reducer.back()}>Back</button> + <button onClick={() => reducer.transition("pay", {})}> + Check payment(s) + </button> </div> </div> ); } - if ( - reducer.currentReducerState.backup_state === "USER_ATTRIBUTES_COLLECTING" - ) { - return <AttributeEntry reducer={reducer} />; - } - if (reducer.currentReducerState.backup_state === "AUTHENTICATIONS_EDITING") { - return <AuthenticationEditor reducer={reducer} />; + if (reducerState.backup_state === BackupStates.PoliciesPaying) { + const backupState: ReducerStateBackup = reducerState; + const payments = backupState.policy_payment_requests ?? []; + return ( + <div class={style.home}> + <h1>Backup: Recovery Document Payments</h1> + <p> + Some of the providers require a payment to store the encrypted + recovery document. + </p> + <ul> + {payments.map((x) => { + return ( + <li> + {x.provider}: {x.payto} + </li> + ); + })} + </ul> + <div> + <button onClick={() => reducer.back()}>Back</button> + <button onClick={() => reducer.transition("pay", {})}> + Check payment(s) + </button> + </div> + </div> + ); } console.log("unknown state", reducer.currentReducerState); @@ -82,31 +292,232 @@ const Home: FunctionalComponent = () => { <div class={style.home}> <h1>Home</h1> <p>Bug: Unknown state.</p> + <button onClick={() => reducer.reset()}>Reset</button> </div> ); }; +interface AuthMethodSetupProps { + method: string; + addAuthMethod: (x: any) => void; + cancel: () => void; +} + +function AuthMethodSmsSetup(props: AuthMethodSetupProps) { + const [mobileNumber, setMobileNumber] = useState(""); + return ( + <div class={style.home}> + <h1>Add {props.method} authentication</h1> + <div> + <p> + For SMS authentication, you need to provide a mobile number. When + recovering your secret, you will be asked to enter the code you + receive via SMS. + </p> + <label> + Mobile number{" "} + <input + value={mobileNumber} + autoFocus + onChange={(e) => setMobileNumber((e.target as any).value)} + type="text" + /> + </label> + <div> + <button onClick={() => props.cancel()}>Cancel</button> + <button + onClick={() => + props.addAuthMethod({ + authentication_method: { + type: "sms", + instructions: `SMS to ${mobileNumber}`, + challenge: "E1QPPS8A", + }, + }) + } + > + Add + </button> + </div> + </div> + </div> + ); +} + +function AuthMethodQuestionSetup(props: AuthMethodSetupProps) { + const [questionText, setQuestionText] = useState(""); + const [answerText, setAnswerText] = useState(""); + return ( + <div class={style.home}> + <h1>Add {props.method} authentication</h1> + <div> + <p> + For security question authentication, you need to provide a question + and its answer. When recovering your secret, you will be shown the + question and you will need to type the answer exactly as you typed it + here. + </p> + <div> + <label> + Security question + <input + value={questionText} + autoFocus + onChange={(e) => setQuestionText((e.target as any).value)} + type="text" + /> + </label> + </div> + <div> + <label> + Answer + <input + value={answerText} + autoFocus + onChange={(e) => setAnswerText((e.target as any).value)} + type="text" + /> + </label> + </div> + <div> + <button onClick={() => props.cancel()}>Cancel</button> + <button + onClick={() => + props.addAuthMethod({ + authentication_method: { + type: "question", + instructions: questionText, + challenge: encodeCrock(stringToBytes(answerText)), + }, + }) + } + > + Add + </button> + </div> + </div> + </div> + ); +} + +function AuthMethodNotImplemented(props: AuthMethodSetupProps) { + return ( + <div class={style.home}> + <h1>Add {props.method} authentication</h1> + <div> + <p> + This auth method is not implemented yet, please choose another one. + </p> + <button onClick={() => props.cancel()}>Cancel</button> + </div> + </div> + ); +} + export interface AuthenticationEditorProps { reducer: AnastasisReducerApi; + backupState: ReducerStateBackup; } function AuthenticationEditor(props: AuthenticationEditorProps) { - const { reducer } = props; - const providers = reducer.currentReducerState.authentication_providers; - const authAvailable = new Set<string>(); + const [selectedMethod, setSelectedMethod] = useState<string | undefined>( + undefined, + ); + const { reducer, backupState } = props; + const providers = backupState.authentication_providers; + const authAvailableSet = new Set<string>(); for (const provKey of Object.keys(providers)) { const p = providers[provKey]; for (const meth of p.methods) { - authAvailable.add(meth.type); + authAvailableSet.add(meth.type); + } + } + if (selectedMethod) { + const cancel = () => setSelectedMethod(undefined); + const addMethod = (args: any) => { + reducer.transition("add_authentication", args); + setSelectedMethod(undefined); + }; + switch (selectedMethod) { + case "sms": + return ( + <AuthMethodSmsSetup + cancel={cancel} + addAuthMethod={addMethod} + method="sms" + /> + ); + case "question": + return ( + <AuthMethodQuestionSetup + cancel={cancel} + addAuthMethod={addMethod} + method="sms" + /> + ); + default: + return ( + <AuthMethodNotImplemented + cancel={cancel} + addAuthMethod={addMethod} + method={selectedMethod} + /> + ); } } + function MethodButton(props: { method: string; label: String }) { + return ( + <button + disabled={!authAvailableSet.has(props.method)} + onClick={() => { + setSelectedMethod(props.method); + reducer.dismissError(); + }} + > + {props.label} + </button> + ); + } + const configuredAuthMethods: AuthMethod[] = + backupState.authentication_methods ?? []; + const haveMethodsConfigured = configuredAuthMethods.length; return ( <div class={style.home}> <h1>Backup: Configure Authentication Methods</h1> - <p>Auths available: {JSON.stringify(Array.from(authAvailable))}</p> - <button>Next</button> + <ErrorBanner reducer={reducer} /> + <h2>Add authentication method</h2> + <div> + <MethodButton method="sms" label="SMS" /> + <MethodButton method="email" label="Email" /> + <MethodButton method="question" label="Question" /> + <MethodButton method="post" label="Physical Mail" /> + <MethodButton method="totp" label="TOTP" /> + <MethodButton method="iban" label="IBAN" /> + </div> + <h2>Configured authentication methods</h2> + {haveMethodsConfigured ? ( + configuredAuthMethods.map((x, i) => { + return ( + <p> + {x.type} ({x.instructions}){" "} + <button + onClick={() => + reducer.transition("delete_authentication", { + authentication_method: i, + }) + } + > + Delete + </button> + </p> + ); + }) + ) : ( + <p>No authentication methods configured yet.</p> + )} <div> <button onClick={() => reducer.back()}>Back</button> + <button onClick={() => reducer.transition("next", {})}>Next</button> </div> </div> ); @@ -114,19 +525,21 @@ function AuthenticationEditor(props: AuthenticationEditorProps) { export interface AttributeEntryProps { reducer: AnastasisReducerApi; + backupState: ReducerStateBackup; } function AttributeEntry(props: AttributeEntryProps) { - const reducer = props.reducer; + const { reducer, backupState } = props; const [attrs, setAttrs] = useState<Record<string, string>>({}); return ( <div class={style.home}> <h1>Backup: Enter Basic User Attributes</h1> <ErrorBanner reducer={reducer} /> <div> - {reducer.currentReducerState.required_attributes.map((x: any) => { + {backupState.required_attributes.map((x: any, i: number) => { return ( <AttributeEntryField + isFirst={i == 0} setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })} spec={x} value={attrs[x.name]} @@ -134,23 +547,24 @@ function AttributeEntry(props: AttributeEntryProps) { ); })} </div> - <button - onClick={() => - reducer.transition("enter_user_attributes", { - identity_attributes: attrs, - }) - } - > - Next - </button> <div> <button onClick={() => reducer.back()}>Back</button> + <button + onClick={() => + reducer.transition("enter_user_attributes", { + identity_attributes: attrs, + }) + } + > + Next + </button> </div> </div> ); } export interface AttributeEntryFieldProps { + isFirst: boolean; value: string; setValue: (newValue: string) => void; spec: any; @@ -161,6 +575,7 @@ function AttributeEntryField(props: AttributeEntryFieldProps) { <div> <label>{props.spec.label}</label> <input + autoFocus={props.isFirst} type="text" value={props.value} onChange={(e) => props.setValue((e as any).target.value)} @@ -179,7 +594,14 @@ interface ErrorBannerProps { function ErrorBanner(props: ErrorBannerProps) { const currentError = props.reducer.currentError; if (currentError) { - return <div>Error: {JSON.stringify(currentError)}</div>; + return ( + <div id={style.error}> + <p>Error: {JSON.stringify(currentError)}</p> + <button onClick={() => props.reducer.dismissError()}> + Dismiss Error + </button> + </div> + ); } return null; } diff --git a/packages/anastasis-webui/src/routes/home/style.css b/packages/anastasis-webui/src/routes/home/style.css index f052d2546..c9f34e6c8 100644 --- a/packages/anastasis-webui/src/routes/home/style.css +++ b/packages/anastasis-webui/src/routes/home/style.css @@ -1,5 +1,24 @@ .home { - padding: 56px 20px; - min-height: 100%; - width: 100%; + padding: 56px 20px; + min-height: 100%; + width: 100%; +} + +.home div { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.policy { + padding: 0.5em; + border: 1px solid black; + border-radius: 0.5em; + border-radius: 0.5em; +} + +.home > #error { + padding: 0.5em; + border: 1px solid black; + background-color: rgb(228, 189, 197); + border-radius: 0.5em; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8f1fd547..fbd3c7e98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,7 @@ importers: packages/anastasis-webui: specifiers: + '@gnu-taler/taler-util': workspace:^0.8.3 '@types/enzyme': ^3.10.5 '@types/jest': ^26.0.8 '@typescript-eslint/eslint-plugin': ^2.25.0 @@ -44,6 +45,7 @@ importers: sirv-cli: ^1.0.0-next.3 typescript: ^3.7.5 dependencies: + '@gnu-taler/taler-util': link:../taler-util preact: 10.5.14 preact-render-to-string: 5.1.19_preact@10.5.14 preact-router: 3.2.1_preact@10.5.14 @@ -4599,7 +4601,7 @@ packages: dependencies: '@types/estree': 0.0.39 estree-walker: 1.0.1 - picomatch: 2.2.2 + picomatch: 2.3.0 rollup: 2.56.2 dev: true @@ -7681,7 +7683,7 @@ packages: /axios/0.21.1: resolution: {integrity: sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==} dependencies: - follow-redirects: 1.14.2 + follow-redirects: 1.14.2_debug@4.3.2 transitivePeerDependencies: - debug @@ -10771,18 +10773,18 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 dependencies: - array-includes: 3.1.2 + array-includes: 3.1.3 array.prototype.flatmap: 1.2.4 doctrine: 2.1.0 eslint: 6.8.0 has: 1.0.3 jsx-ast-utils: 3.2.0 - object.entries: 1.1.3 - object.fromentries: 2.0.3 - object.values: 1.1.2 + object.entries: 1.1.4 + object.fromentries: 2.0.4 + object.values: 1.1.4 prop-types: 15.7.2 - resolve: 1.19.0 - string.prototype.matchall: 4.0.3 + resolve: 1.20.0 + string.prototype.matchall: 4.0.5 dev: true /eslint-plugin-react/7.22.0_eslint@7.18.0: @@ -11444,7 +11446,7 @@ packages: readable-stream: 2.3.7 dev: true - /follow-redirects/1.14.2: + /follow-redirects/1.14.2_debug@4.3.2: resolution: {integrity: sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==} engines: {node: '>=4.0'} peerDependencies: @@ -11452,6 +11454,8 @@ packages: peerDependenciesMeta: debug: optional: true + dependencies: + debug: 4.3.2_supports-color@6.1.0 /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -12485,7 +12489,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.14.2 + follow-redirects: 1.14.2_debug@4.3.2 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -14131,7 +14135,7 @@ packages: resolution: {integrity: sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==} engines: {node: '>=4.0'} dependencies: - array-includes: 3.1.2 + array-includes: 3.1.3 object.assign: 4.1.2 dev: true @@ -15962,11 +15966,11 @@ packages: - typescript dev: true - /pnp-webpack-plugin/1.7.0_typescript@4.3.5: + /pnp-webpack-plugin/1.7.0_typescript@4.4.3: resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==} engines: {node: '>=6'} dependencies: - ts-pnp: 1.2.0_typescript@4.3.5 + ts-pnp: 1.2.0_typescript@4.4.3 transitivePeerDependencies: - typescript dev: true @@ -16770,7 +16774,7 @@ packages: native-url: 0.3.4 optimize-css-assets-webpack-plugin: 6.0.1_webpack@4.46.0 ora: 5.4.1 - pnp-webpack-plugin: 1.7.0_typescript@4.3.5 + pnp-webpack-plugin: 1.7.0_typescript@4.4.3 postcss: 8.3.6 postcss-load-config: 3.1.0 postcss-loader: 4.3.0_postcss@8.3.6+webpack@4.46.0 @@ -16788,7 +16792,7 @@ packages: stack-trace: 0.0.10 style-loader: 2.0.0_webpack@4.46.0 terser-webpack-plugin: 4.2.3_webpack@4.46.0 - typescript: 4.3.5 + typescript: 4.4.3 update-notifier: 5.1.0 url-loader: 4.1.1_file-loader@6.2.0+webpack@4.46.0 validate-npm-package-name: 3.0.0 @@ -18039,11 +18043,11 @@ packages: peerDependencies: rollup: ^2.0.0 dependencies: - '@babel/code-frame': 7.12.13 + '@babel/code-frame': 7.14.5 jest-worker: 26.6.2 rollup: 2.56.2 serialize-javascript: 4.0.0 - terser: 5.4.0 + terser: 5.7.1 dev: true /rollup/2.37.1: @@ -19167,6 +19171,7 @@ packages: /svgo/1.3.2: resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==} engines: {node: '>=4.0.0'} + deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x. hasBin: true dependencies: chalk: 2.4.2 @@ -19588,6 +19593,18 @@ packages: typescript: 4.3.5 dev: true + /ts-pnp/1.2.0_typescript@4.4.3: + resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==} + engines: {node: '>=6'} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 4.4.3 + dev: true + /tsconfig-paths/3.9.0: resolution: {integrity: sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==} dependencies: |