> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rhinestone.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up

> Configure verified-identity recovery and validate an app-origin session.

export const CSS = ".rhinestone-1auth-demo .oa-demo {\n  margin: 1.5rem 0;\n  border: 1px solid #202127;\n  border-radius: 16px;\n  background: #0b0c0e;\n  overflow: hidden;\n}\n.rhinestone-1auth-demo .oa-demo-head {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 0.75rem;\n  min-height: 39px;\n  padding: 0.625rem 1rem;\n  border-bottom: 1px solid #191a1e;\n}\n.rhinestone-1auth-demo .oa-demo-title {\n  font-size: 0.8125rem;\n  font-weight: 500;\n  letter-spacing: -0.01em;\n  color: #e4e4e9;\n}\n.rhinestone-1auth-demo .oa-demo-status {\n  display: flex;\n  align-items: center;\n  gap: 0.4375rem;\n  flex: 0 0 auto;\n  font-size: 0.75rem;\n  color: #5e5e68;\n}\n.rhinestone-1auth-demo .oa-demo-dot {\n  width: 6px;\n  height: 6px;\n  border-radius: 999px;\n  background: #3f3f48;\n}\n.rhinestone-1auth-demo .oa-demo-dot[data-state=\"on\"] {\n  background: #22c55e;\n  box-shadow: 0 0 8px rgba(34, 197, 94, 0.5);\n}\n.rhinestone-1auth-demo .oa-demo-body {\n  display: flex;\n  flex-direction: column;\n  gap: 0.75rem;\n  padding: 1rem;\n}\n.rhinestone-1auth-demo .demo-container {\n  display: contents;\n}\n.rhinestone-1auth-demo .oa-demo-body > * > [style] {\n  margin: 0 !important;\n}\n.rhinestone-1auth-demo .oa-demo-body > * > div[style]:not([class]) {\n  padding: 0 !important;\n}\n.rhinestone-1auth-demo .oa-demo-body [style] {\n  text-align: left !important;\n}\n.rhinestone-1auth-demo .oa-demo-body [style*=\"justify-content: center\"] {\n  justify-content: flex-start !important;\n}\n.rhinestone-1auth-demo .demo-button {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  gap: 0.375rem;\n  height: 2.25rem;\n  padding: 0 0.875rem;\n  border: none;\n  border-radius: 10px;\n  background: #4f41ef;\n  color: #ffffff;\n  font-size: 0.875rem;\n  font-weight: 500;\n  cursor: pointer;\n  transition:\n    background 0.15s ease,\n    opacity 0.15s ease;\n}\n.rhinestone-1auth-demo .demo-button:hover:not(:disabled) {\n  background: #6355f5;\n}\n.rhinestone-1auth-demo .demo-button:disabled {\n  opacity: 0.6;\n  cursor: default;\n}\n.rhinestone-1auth-demo .oa-demo-body > * > .demo-button {\n  align-self: flex-start;\n}\n.rhinestone-1auth-demo .oa-demo-body input[type=\"checkbox\"] {\n  accent-color: #4f41ef;\n}\n.rhinestone-1auth-demo .demo-result {\n  padding: 0.75rem 0.875rem;\n  border-radius: 10px;\n  background: #14151a;\n}\n.rhinestone-1auth-demo .demo-result-label {\n  font-size: 0.6875rem;\n  font-weight: 500;\n  letter-spacing: 0.08em;\n  text-transform: uppercase;\n  color: #6e6e78;\n}\n.rhinestone-1auth-demo .demo-result-value {\n  margin-top: 0.1875rem;\n  font-family: paperMono, ui-monospace, SFMono-Regular, Menlo, monospace;\n  font-size: 0.8125rem;\n  line-height: 1.5;\n  color: #e4e4e9;\n  overflow-wrap: anywhere;\n}\n.rhinestone-1auth-demo .demo-error {\n  margin: 0;\n  font-size: 0.8125rem;\n  color: #f87171;\n  overflow-wrap: anywhere;\n}\n.rhinestone-1auth-demo .oa-demo-skeleton {\n  display: block;\n  width: 7rem;\n  height: 2.25rem;\n  border-radius: 10px;\n  background: #16171c;\n  animation: rhinestone-1auth-demo-pulse 1.6s ease-in-out infinite;\n}\n@keyframes rhinestone-1auth-demo-pulse {\n  50% {\n    opacity: 0.45;\n  }\n}";

export const SOURCE_COMMIT = "e06ebe8add704e95f679eaddc38fd7ea88cb3de8";

export const SDK_VERSION = "0.10.1";

export const VIEM_VERSION = "2.55.0";

export const STORAGE_KEY = "rhinestone-docs-1auth-account";

export const PROD_BACKENDS = {
  auth: "https://passkey.1auth.app",
  sponsorship: "https://docs.1auth.app/api/sponsorship"
};

export const DEV_BACKENDS = {
  auth: "https://passkey.passkey-test.app",
  sponsorship: "https://docs.passkey-test.app/api/sponsorship"
};

export const PROD_DOCS_HOST = "docs.rhinestone.dev";

export const backends = () => {
  const host = typeof window === "undefined" ? "" : window.location.hostname;
  return host === PROD_DOCS_HOST ? PROD_BACKENDS : DEV_BACKENDS;
};

export const BACKEND_NOTE = "Only the production docs host talks to the production Rhinestone deployment. " + "Every other host — a Mintlify preview, a tunnel, localhost — uses dev, so " + "an unrecognised preview pattern fails towards dev rather than creating " + "passkey state against production.";

export const CDN = spec => "https://esm.sh/" + spec + (spec.startsWith("@rhinestone/1auth@") ? "?deps=viem@" + VIEM_VERSION + "&external=react" : "");

export const shareReact = () => {
  if (window.__OA_SHARED_REACT__) return;
  if (typeof HTMLScriptElement.supports !== "function" || !HTMLScriptElement.supports("importmap")) {
    throw new Error("This demo requires a browser with import-map support.");
  }
  window.__OA_REACT__ = React;
  const reserved = ["default", "import", "export", "class", "function", "const", "let", "var", "new", "delete", "typeof", "void", "in", "of", "do", "if", "else", "for", "while", "switch", "case", "break", "continue", "return", "this", "super", "throw", "try", "catch", "finally", "yield", "await", "enum", "null", "true", "false", "with", "debugger", "instanceof", "extends", "static"];
  const names = Object.keys(React).filter(name => (/^[A-Za-z_$][A-Za-z0-9_$]*$/).test(name) && !reserved.includes(name));
  const source = ["const R = globalThis.__OA_REACT__; export default R;", ...names.map(name => "export const " + name + " = R." + name + ";")].join(" ");
  const jsx = ["const R = globalThis.__OA_REACT__; export const Fragment = R.Fragment;", "export const jsx = (type, props, key) => R.createElement(type, key == null ? props : { ...props, key });", "export const jsxs = jsx; export const jsxDEV = jsx;"].join(" ");
  const dataUrl = value => "data:text/javascript," + encodeURIComponent(value);
  const script = document.createElement("script");
  script.type = "importmap";
  script.textContent = JSON.stringify({
    imports: {
      react: dataUrl(source),
      "react/jsx-runtime": dataUrl(jsx),
      "react/jsx-dev-runtime": dataUrl(jsx)
    }
  });
  document.head.appendChild(script);
  window.__OA_SHARED_REACT__ = true;
};

export const MODULE_SPECS = {
  sdk: () => CDN("@rhinestone/1auth@" + SDK_VERSION)
};

export const importModule = url => new Promise((resolve, reject) => {
  const key = "__oa_" + Math.random().toString(36).slice(2);
  window[key] = {
    resolve,
    reject
  };
  const script = document.createElement("script");
  script.type = "module";
  script.textContent = "import(" + JSON.stringify(url) + ").then(m => window[" + JSON.stringify(key) + "].resolve(m))" + ".catch(e => window[" + JSON.stringify(key) + "].reject(new Error(String(e))));";
  script.onerror = () => reject(new Error("failed to load " + url));
  document.head.appendChild(script);
  script.remove();
});

export const importModuleWithRetry = async url => {
  try {
    return await importModule(url);
  } catch (first) {
    await new Promise(r => setTimeout(r, 500));
    return importModule(url);
  }
};

export const loadDeps = async () => {
  shareReact();
  const specs = Object.entries(MODULE_SPECS).map(([name, spec]) => [name, spec()]);
  const cacheKey = "__OA_DEPS__" + specs.map(([, url]) => url).join("|");
  if (window[cacheKey]) return window[cacheKey];
  const loaded = await Promise.all(specs.map(([, url]) => importModuleWithRetry(url)));
  const deps = {
    React
  };
  specs.forEach(([name], i) => {
    deps[name] = loaded[i];
  });
  deps.motion = motionShim();
  window[cacheKey] = deps;
  return deps;
};

export const unreachable = call => "Sponsorship " + call + " never reached " + backends().sponsorship + ". The browser hides why: a rejected origin, a failed preflight and a network" + " error all look identical here. The failing request is in the console.";

export const sponsorship = {
  accessToken: async () => {
    let res;
    try {
      res = await fetch(backends().sponsorship + "/access-token", {
        method: "GET",
        credentials: "include"
      });
    } catch (e) {
      throw new Error(unreachable("access token"));
    }
    if (!res.ok) throw new Error("Sponsorship access token failed (" + res.status + ")");
    return (await res.json()).token;
  },
  getExtensionToken: async intentOp => {
    let res;
    try {
      res = await fetch(backends().sponsorship + "/extension-token", {
        method: "POST",
        credentials: "include",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          intentOp
        })
      });
    } catch (e) {
      throw new Error(unreachable("extension token"));
    }
    if (!res.ok) throw new Error("Sponsorship extension token failed (" + res.status + ")");
    return (await res.json()).token;
  }
};

export const motionShim = () => {
  const passthrough = Tag => React.forwardRef((props, ref) => {
    const rest = Object.fromEntries(Object.entries(props).filter(([k]) => !["initial", "animate", "exit", "transition", "variants", "whileHover", "whileTap", "layout", "layoutId"].includes(k)));
    return React.createElement(Tag, {
      ...rest,
      ref
    });
  });
  const cache = {};
  return {
    motion: new Proxy({}, {
      get: (_, tag) => {
        if (!cache[tag]) cache[tag] = passthrough(tag);
        return cache[tag];
      }
    }),
    AnimatePresence: ({children}) => React.createElement(React.Fragment, null, children),
    useReducedMotion: () => true
  };
};

export const AuthContext = React.createContext(null);

export const useAuthContext = () => React.useContext(AuthContext);

export const buildModules = deps => {
  const mods = {};
  mods.authctx = (() => {
    const DOCS_APP_ORIGIN_STORAGE_KEY = STORAGE_KEY;
    const useAuth = () => useAuthContext();
    return {
      DOCS_APP_ORIGIN_STORAGE_KEY,
      useAuth,
      clearDocsSession: () => {}
    };
  })();
  mods.accountRecoveryDemo = (() => {
    const {useCallback, useState} = deps.React;
    const {useAuth} = mods.authctx;
    function AccountRecoveryDemo() {
      const {client, isAuthenticated, login} = useAuth();
      const [state, setState] = useState({
        status: "idle"
      });
      const checkRecovery = useCallback(async () => {
        if (!client) return;
        setState({
          status: "working"
        });
        try {
          if (!isAuthenticated && !await login()) {
            setState({
              status: "idle"
            });
            return;
          }
          const result = await client.setupRecovery();
          setState(result.completed ? {
            status: "validated"
          } : {
            status: "error",
            message: "App-origin session validation was not confirmed."
          });
        } catch (error) {
          setState({
            status: "error",
            message: error instanceof Error ? error.message : "Recovery check failed"
          });
        }
      }, [client, isAuthenticated, login]);
      const working = state.status === "working";
      return React.createElement("div", {
        className: "demo-container",
        "data-testid": "account-recovery-demo"
      }, React.createElement("div", {
        className: "demo-result",
        "data-testid": "account-recovery-result"
      }, React.createElement("p", {
        className: "demo-result-label"
      }, "App-origin session"), React.createElement("p", {
        className: "demo-result-value",
        "data-testid": "account-recovery-status"
      }, state.status === "validated" ? "App-origin session validated. This does not inspect guardians or recovery configuration." : state.status === "error" ? state.message : isAuthenticated ? "Signed in. Validate the current app-origin session." : "Log in to validate an app-origin session for this docs-hostname account.")), React.createElement("button", {
        className: "demo-button",
        "data-testid": "account-recovery-action",
        disabled: !client || working,
        onClick: () => void checkRecovery(),
        style: {
          marginTop: "0.75rem"
        }
      }, working ? "Validating\u2026" : isAuthenticated ? "Validate app-origin session" : "Log in and validate app-origin session"));
    }
    return {
      AccountRecoveryDemo
    };
  })();
  return mods;
};

export const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;

export const readStoredAddressFromJsonEnvelope = () => {
  try {
    const raw = window.localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    const address = parsed && typeof parsed === "object" ? parsed.address : null;
    return typeof address === "string" && ADDRESS_RE.test(address) ? address : null;
  } catch {
    return null;
  }
};

export const writeStoredAddressAsJsonEnvelope = address => {
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify({
    address
  }));
};

export const useOneAuth = deps => {
  const [address, setAddress] = React.useState(null);
  const [client, setClient] = React.useState(null);
  const mounted = React.useRef(false);
  const sessionGeneration = React.useRef(0);
  React.useEffect(() => {
    if (!deps) return;
    mounted.current = true;
    const restoreGeneration = ++sessionGeneration.current;
    const {OneAuthClient} = deps.sdk;
    const nextClient = new OneAuthClient({
      providerUrl: backends().auth,
      clientId: "docs-app-origin",
      webauthn: {
        mode: "app_origin",
        rpId: window.location.hostname
      },
      theme: {
        mode: "dark",
        accent: "#ffffff",
        backdrop: {
          color: "#52525c",
          opacity: 0.5,
          blur: 12
        }
      },
      experimental_clear_signing: true,
      recovery: {
        fallback: {
          type: "verified-identity"
        }
      },
      testnets: true,
      prewarm: true,
      sponsorship,
      onDisconnect: () => {
        if (!mounted.current) return;
        sessionGeneration.current += 1;
        window.localStorage.removeItem(STORAGE_KEY);
        setAddress(null);
      }
    });
    setClient(nextClient);
    const stored = readStoredAddressFromJsonEnvelope();
    if (stored) {
      nextClient.getSession().then(result => {
        if (!mounted.current || sessionGeneration.current !== restoreGeneration) return;
        const account = result.success ? result.session.accountAddress : null;
        if (account && ADDRESS_RE.test(account)) {
          writeStoredAddressAsJsonEnvelope(account);
          setAddress(account);
        } else {
          window.localStorage.removeItem(STORAGE_KEY);
          setAddress(null);
        }
      }).catch(() => {
        if (!mounted.current || sessionGeneration.current !== restoreGeneration) return;
        window.localStorage.removeItem(STORAGE_KEY);
        setAddress(null);
      });
    }
    return () => {
      mounted.current = false;
      sessionGeneration.current += 1;
    };
  }, [deps]);
  const login = React.useCallback(async () => {
    if (!client) return null;
    const generation = ++sessionGeneration.current;
    const result = await client.authenticate({
      oauthEnabled: false
    });
    if (!result.success) {
      if (result.error?.code === "USER_CANCELLED") return null;
      throw new Error(result.error?.message ?? "Authentication failed");
    }
    const account = result.session.accountAddress;
    if (!ADDRESS_RE.test(account)) throw new Error("Authentication returned an invalid account address");
    if (!mounted.current || sessionGeneration.current !== generation) return null;
    writeStoredAddressAsJsonEnvelope(account);
    setAddress(account);
    return account;
  }, [client]);
  const logout = React.useCallback(() => {
    sessionGeneration.current += 1;
    window.localStorage.removeItem(STORAGE_KEY);
    if (mounted.current) setAddress(null);
    if (client?.disconnect) client.disconnect();
  }, [client]);
  return React.useMemo(() => ({
    address,
    isAuthenticated: !!address,
    client,
    sponsorship,
    login,
    logout
  }), [address, client, login, logout]);
};

export const Frame = ({children, connected}) => <div className="oa-demo">
    <div className="oa-demo-head">
      <span className="oa-demo-title">Validate app-origin session</span>
      <span className="oa-demo-status">
        <span className="oa-demo-dot" data-state={connected ? "on" : "off"} />
        {connected ? "Signed in" : "Not connected"}
      </span>
    </div>
    <div className="oa-demo-body">{children}</div>
  </div>;

export const AccountRecoveryDemo = () => {
  const [deps, setDeps] = React.useState(null);
  const [loadError, setLoadError] = React.useState("");
  const [connecting, setConnecting] = React.useState(false);
  React.useEffect(() => {
    let cancelled = false;
    loadDeps().then(d => !cancelled && setDeps(d)).catch(e => !cancelled && setLoadError(String(e.message ?? e)));
    return () => {
      cancelled = true;
    };
  }, []);
  const mods = React.useMemo(() => deps ? buildModules(deps) : null, [deps]);
  const auth = useOneAuth(deps);
  return <div className="not-prose rhinestone-1auth-demo" data-demo="account-recovery">
      <style dangerouslySetInnerHTML={{
    __html: CSS
  }} />
      <AuthContext.Provider value={auth}>
        <Frame connected={auth.isAuthenticated}>
          {mods && auth.client && false && !auth.isAuthenticated ? <div>
              <button className="demo-button" disabled={connecting} onClick={async () => {
    setConnecting(true);
    setLoadError("");
    try {
      await auth.login();
    } catch (error) {
      setLoadError(String(error.message ?? error));
    } finally {
      setConnecting(false);
    }
  }}>{connecting ? "Signing in..." : "Sign in to try it"}</button>
              {loadError ? <p className="demo-error">{loadError}</p> : null}
            </div> : mods && (auth.client || false) ? React.createElement(mods.accountRecoveryDemo.AccountRecoveryDemo, {
    key: auth.address
  }) : loadError ? <p className="demo-error">{loadError}</p> : <span className="oa-demo-skeleton" />}
        </Frame>
      </AuthContext.Provider>
    </div>;
};

Configure recovery when an app-origin account is created. Verified-identity recovery in the public `@rhinestone/1auth` `0.10.1` release lets a user who loses every passkey prove the same email or OAuth identity and add a new passkey to that application's existing smart account.

This managed-wallet flow is separate from [SDK guardian recovery](/wallets/custom-signer/recovery/set-up), where your application installs and operates its own onchain guardian accounts.

## Configure account creation

Add the verified-identity fallback to the same client used for sign-up and login. `clientConfig` is your provider, client ID, and sponsorship configuration from the shared [client initialization](/wallets/embedded-wallets/accounts#initialize-the-client):

```ts {4} theme={null}
export const oneAuth = new OneAuthClient({
  ...clientConfig,
  recovery: {
    fallback: { type: "verified-identity" },
  },
});
```

During account creation, the embedded wallet SDK first attempts passkey-based recovery using the passkey provider's PRF capability. The configured fallback applies only when passkey-based recovery is unavailable.

`recovery.fallback` is an account-creation policy. Adding it to a client later does not retrofit recovery onto an already-created account.

<Warning>
  Keep the account namespace unchanged. `clientId` selects registered
  application metadata; it does not move recovery to the hosted provider's
  centralized WebAuthn namespace. Changing the host name, RP ID, or WebAuthn
  mode selects another credential, signer, and smart account.
</Warning>

## Validate the app-origin session

Call `setupRecovery()` after a successful login or account creation when you need to validate the current app-origin session and namespace:

```ts theme={null}
const auth = await oneAuth.authenticate();
if (!auth.success) throw new Error(auth.error.message);

const sessionValidation = await oneAuth.setupRecovery();
if (!sessionValidation.completed) {
  throw new Error(sessionValidation.error.message);
}
```

For an app-origin account, the method calls the SDK's app-origin session requirement and then returns `completed: true`. It does not inspect the account's guardian or recovery configuration. An already-created account without a recovery guardian can therefore return `completed: true`.

Do not use this result to gate a “recovery ready” state. It proves only that the current origin, RP ID, and account namespace have a valid session. It does not perform lost-passkey recovery, create a guardian, or open the centralized recovery-passphrase and backup-file flow.

<AccountRecoveryDemo />

<Note>
  On a client explicitly using `webauthn: { mode: "experimental_cross_origin" }`, `setupRecovery()` has different behavior: it opens the provider-owned backup flow. Do not switch modes to reach that UI because the modes use different accounts.
</Note>

Continue with [Recover an account](/wallets/recovery/recover-an-account) for the flow after every usable passkey is lost.
