import { useState } from "preact/hooks"; export type ReducerState = | ReducerStateBackup | ReducerStateRecovery | ReducerStateError; export interface ReducerStateBackup { recovery_state: undefined; backup_state: BackupStates; code: undefined; continents: any; countries: any; identity_attributes?: { [n: string]: string }; 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; }[]; core_secret?: { mime: string; value: string; }; } export interface AuthMethod { type: string; instructions: string; challenge: string; } export interface ChallengeInfo { cost: string; instructions: string; type: string; uuid: string; } export interface ReducerStateRecovery { backup_state: undefined; recovery_state: RecoveryStates; code: undefined; identity_attributes?: { [n: string]: string }; continents: any; countries: any; required_attributes: any; recovery_information?: { challenges: ChallengeInfo[]; policies: { /** * UUID of the associated challenge. */ uuid: string; }[][]; }; recovery_document?: { secret_name: string; provider_url: string; version: number; }; selected_challenge_uuid?: string; challenge_feedback?: { [uuid: string]: ChallengeFeedback }; core_secret?: { mime: string; value: string; }; authentication_providers?: { [url: string]: { business_name: string; }; }; recovery_error: any; } export interface ChallengeFeedback { state: string; } export interface ReducerStateError { backup_state: undefined; recovery_state: undefined; code: number; } interface AnastasisState { reducerState: ReducerState | undefined; currentError: any; } 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 { ContinentSelecting = "CONTINENT_SELECTING", CountrySelecting = "COUNTRY_SELECTING", UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING", SecretSelecting = "SECRET_SELECTING", ChallengeSelecting = "CHALLENGE_SELECTING", ChallengePaying = "CHALLENGE_PAYING", ChallengeSolving = "CHALLENGE_SOLVING", RecoveryFinished = "RECOVERY_FINISHED", } const reducerBaseUrl = "http://localhost:5000/"; async function getBackupStartState(): Promise { const resp = await fetch(new URL("start-backup", reducerBaseUrl).href); return await resp.json(); } async function getRecoveryStartState(): Promise { const resp = await fetch(new URL("start-recovery", reducerBaseUrl).href); return await resp.json(); } async function reduceState( state: any, action: string, args: any, ): Promise { const resp = await fetch(new URL("action", reducerBaseUrl).href, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ state, action, arguments: args, }), }); return resp.json(); } export interface ReducerTransactionHandle { transactionState: ReducerState; transition(action: string, args: any): Promise; } export interface AnastasisReducerApi { 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; } 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, setAnastasisStateInternal] = useState( () => ({ 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); const s = await reduceState(anastasisState.reducerState, action, args); console.log("got new state from reducer", s); if (s.code) { setAnastasisState({ ...anastasisState, currentError: s }); } else { setAnastasisState({ ...anastasisState, currentError: undefined, reducerState: s, }); } } return { currentReducerState: anastasisState.reducerState, currentError: anastasisState.currentError, async startBackup() { const s = await getBackupStartState(); setAnastasisState({ ...anastasisState, currentError: undefined, reducerState: s, }); }, async startRecover() { const s = await getRecoveryStartState(); setAnastasisState({ ...anastasisState, currentError: undefined, reducerState: s, }); }, transition(action: string, args: any) { doTransition(action, args); }, back() { const reducerState = anastasisState.reducerState; if (!reducerState) { return; } if ( reducerState.backup_state === BackupStates.ContinentSelecting || reducerState.recovery_state === RecoveryStates.ContinentSelecting ) { setAnastasisState({ ...anastasisState, currentError: undefined, reducerState: undefined, }); } 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 { 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; } }