@seamless-auth/react is a React SDK for Seamless Auth. It gives you a provider for auth state, a headless client and hooks for custom auth UIs, and optional prebuilt auth routes when you want a faster drop-in flow.
AuthProviderAuthRoutesuseAuth()createSeamlessAuthClient()useAuthClient()usePasskeySupport()hasScopedRole()androleGrantsAccess()SeamlessAuthError, the error type carried on a failed resultgetOAuthErrorCode(), which reads the known OAuth callback failure codes off that errorgetWebAuthnErrorDetail(), which reads the underlying failure of a passkey or step-up ceremony- types including
AuthContextType,Credential,User,OAuthProvider,StepUpStatus, theSeamlessAuthResultwrapper, and the headless client input/result types
npm install @seamless-auth/reactPublished versions are listed in CHANGELOG.md and GitHub Releases. Releases are
managed with Changesets: adopter-facing changes include a changeset, the Release workflow opens a
version PR for review, and merging that PR publishes the npm package with provenance from GitHub
Actions. See RELEASES.md for maintainer release details.
You can use this package in three ways:
AuthProvider+useAuth()for auth state and core auth actionscreateSeamlessAuthClient()oruseAuthClient()to build fully custom login and registration screensAuthRouteswhen you want the built-in login, OTP, magic-link, and passkey screens
Most apps will use AuthProvider either way.
import{AuthProvider}from'@seamless-auth/react';import{BrowserRouter}from'react-router-dom';<BrowserRouter><AuthProviderapiHost="https://your.api"><AppRoutes/></AuthProvider></BrowserRouter>;import{useAuth}from'@seamless-auth/react';functionDashboard(){const{ user, logout, refreshSession }=useAuth();return(<div><p>Welcome, {user?.email}</p><buttononClick={()=>voidrefreshSession()}>Refresh session</button><buttononClick={()=>voidlogout()}>Logout</button></div>);}import{AuthRoutes,useAuth}from'@seamless-auth/react';import{Route,Routes}from'react-router-dom';functionAppRoutes(){const{ isAuthenticated }=useAuth();return(<Routes>{isAuthenticated ? (<Routepath="*"element={<Dashboard/>}/>) : (<Routepath="*"element={<AuthRoutes/>}/>)}</Routes>);}You are still responsible for your app’s route protection and redirects.
useAuth() returns the current auth state plus the provider-backed helpers:
{
user: User|null;
credentials: Credential[];
stepUpStatus: StepUpStatus|null;
isAuthenticated: boolean;
loading: boolean;
apiHost: string;
hasSignedInBefore: boolean;markSignedIn(): void;hasRole(role: string): boolean |undefined;hasScopedRole(role: string|string[]): boolean |undefined;listOAuthProviders(): Promise<SeamlessAuthResult<OAuthProvidersResult>>;startOAuthLogin(input: StartOAuthLoginInput): Promise<SeamlessAuthResult<StartOAuthLoginResult>>;finishOAuthLogin(input: FinishOAuthLoginInput): Promise<SeamlessAuthResult<MessageResult>>;refreshSession(): Promise<SeamlessAuthResult<CurrentUserResult>>;refreshStepUpStatus(): Promise<SeamlessAuthResult<StepUpStatus>>;verifyStepUpWithPasskey(): Promise<SeamlessAuthResult<StepUpStatus>>;verifyStepUpWithPasskeyPrf(input: PasskeyPrfInput): Promise<SeamlessAuthResult<StepUpPrfData>>;verifyStepUpWithTotp(code: string): Promise<SeamlessAuthResult<StepUpStatus>>;logout(): Promise<SeamlessAuthResult<MessageResult>>;logoutAllSessions(): Promise<SeamlessAuthResult<MessageResult>>;deleteUser(): Promise<SeamlessAuthResult<MessageResult>>;login(identifier: string,passkeyAvailable: boolean): Promise<SeamlessAuthResult<LoginStartResult>>;handlePasskeyLogin(): Promise<SeamlessAuthResult<PasskeyLoginData>>;updateCredential(credential: Credential): Promise<SeamlessAuthResult<Credential>>;deleteCredential(credentialId: string): Promise<SeamlessAuthResult<MessageResult>>;}Use refreshSession() after completing a custom auth flow that should update provider state.
hasSignedInBefore is a small convenience flag backed by localStorage. The provider reads the seamlessauth_seen key on load and sets the flag to true after markSignedIn() runs.
This is mainly useful for login UIs that want to branch between first-time and returning-user behavior. For example, the built-in Login view uses it to default returning users to sign-in mode instead of registration.
import{useAuth}from'@seamless-auth/react';functionSignInHint(){const{ hasSignedInBefore }=useAuth();returnhasSignedInBefore ? (<p>Welcome back. Sign in with your email, phone, or passkey.</p>) : (<p>New here? Start by creating your account.</p>);}If you are building a fully custom flow, call markSignedIn() after a successful sign-in or registration step once you want future visits treated as returning-user sessions.
const{ markSignedIn, refreshSession }=useAuth();asyncfunctioncompleteLogin(){const{ error }=awaitauthClient.login({identifier: 'user@example.com',passkeyAvailable: true,});if(!error){markSignedIn();awaitrefreshSession();}}To disable this auto-detection entirely, pass autoDetectPreviousSignin={false} to AuthProvider.
hasRole(role) remains an exact role check. Use hasScopedRole(role) for colon-separated scoped
roles such as admin:read and admin:write.
const{ hasRole, hasScopedRole }=useAuth();hasRole('admin');// exact legacy role checkhasScopedRole('admin:read');// true for admin, admin:read, or admin:writehasScopedRole('admin:write');// true for admin or admin:writeThe package also exports standalone hasScopedRole(roles, required) and roleGrantsAccess(...)
helpers for code that is not inside AuthProvider.
Use step-up authentication before sensitive actions that should require a fresh user verification, such as deleting an account, changing MFA settings, or viewing recovery material.
import{useAuth}from'@seamless-auth/react';functionDeleteAccountButton(){const{ refreshStepUpStatus, verifyStepUpWithPasskey }=useAuth();asyncfunctionhandleDeleteAccount(){const{data: status}=awaitrefreshStepUpStatus();constfresh=status?.fresh ? true : !(awaitverifyStepUpWithPasskey()).error;if(!fresh){return;}awaitdeleteAccount();}return<buttononClick={()=>voidhandleDeleteAccount()}>Delete account</button>;}Step-up supports WebAuthn/passkeys and TOTP (authenticator apps). refreshStepUpStatus() calls /step-up/status, verifyStepUpWithPasskey() performs the /step-up/webauthn/start and /step-up/webauthn/finish challenge flow, and verifyStepUpWithTotp(code) verifies a 6-digit authenticator code via /totp/verify-mfa. The verification helpers return a SeamlessAuthResult<StepUpStatus> and refresh the provider's stepUpStatus when they succeed.
const{ verifyStepUpWithTotp }=useAuth();const{ error }=awaitverifyStepUpWithTotp('123456');// 6-digit code from the authenticator appif(!error){// step-up is fresh; proceed with the sensitive action}TOTP lets users register an authenticator app (Google Authenticator, 1Password, etc.) as a second factor for step-up verification. The SDK exposes headless client methods for enrollment and management; use them from a settings screen. All require an authenticated session.
import{createSeamlessAuthClient}from'@seamless-auth/react';importtype{TotpStatus,TotpEnrollmentStartResult}from'@seamless-auth/react';constauthClient=createSeamlessAuthClient({apiHost: 'https://your.api'});// 1. Check whether TOTP is already enabledconst{data: status}=awaitauthClient.getTotpStatus();// 2. Start enrollment: render `otpauthUrl` as a QR code (or show `secret` for manual entry)const{data: enrollment}=awaitauthClient.startTotpEnrollment();// 3. Confirm the first code from the user's authenticator appconst{ error }=awaitauthClient.verifyTotpEnrollment('123456');if(!error){// TOTP is now enabled}// Disabling requires a current codeawaitauthClient.disableTotp('123456');These methods follow the standard result convention: check error, then read data. Enrolling TOTP is a sensitive change; gate it behind a fresh step-up when appropriate.
TOTP is not currently a login second factor. The Seamless Auth API issues a full session on the first factor and does not gate login on TOTP, so TOTP applies to step-up verification, not to the login flow.
WebAuthn PRF lets a compatible passkey and browser derive local key material during a WebAuthn assertion. Seamless Auth verifies the passkey assertion on the server, while the React SDK returns the PRF output only to the browser caller. PRF output is stripped before /webAuthn/login/finish and /step-up/webauthn/finish, and should never be logged, stored, or sent to your API.
Browser and authenticator support is not universal. Call isPasskeyPrfSupported() before offering PRF-required flows, and keep a fallback for passkeys that authenticate successfully without returning PRF output.
Treat PRF salts as sensitive in client logs. PRF output is browser-local key material; keep it in memory only as long as your application needs it and do not send it to Seamless Auth or your own API.
import{createSeamlessAuthClient}from'@seamless-auth/react';constauthClient=createSeamlessAuthClient({apiHost: 'https://your.api',});constprfSupported=awaitauthClient.isPasskeyPrfSupported();if(prfSupported){awaitauthClient.registerPasskey({metadata: {friendlyName: 'My laptop',platform: 'macOS',browser: 'Chrome',deviceInfo: navigator.userAgent,},requirePrf: true,});}For local key unwrap flows such as Seamless Secrets, use PRF during step-up and consume the returned bytes in browser memory:
const{ data, error }=awaitauthClient.verifyStepUpWithPasskeyPrf({salt: vaultSaltBase64url,
credentialId,});if(error){throwerror;}constvaultUnlockMaterial: {credentialId: string;output: Uint8Array}={credentialId: data.credentialId,output: data.prf.output,};The salt may be an ArrayBuffer, ArrayBufferView, or base64url string. Authentication proves identity and user presence; the PRF output is local key material for your application to use without sending it to Seamless Auth.
OAuth lets your app offer external identity providers such as Google, GitHub, Facebook, or custom OIDC-style providers configured on the Seamless Auth API. The React SDK does not receive provider access tokens. It only starts the provider redirect and completes the callback so Seamless Auth can issue the normal access/refresh session.
Use listOAuthProviders() when you want to render enabled providers dynamically:
import{useEffect,useState}from'react';import{useAuth}from'@seamless-auth/react';importtype{OAuthProvider}from'@seamless-auth/react';functionOAuthButtons(){const{ listOAuthProviders, startOAuthLogin }=useAuth();const[providers,setProviders]=useState<OAuthProvider[]>([]);useEffect(()=>{voidlistOAuthProviders().then(result=>setProviders(result.providers));},[listOAuthProviders]);asyncfunctionsignIn(providerId: string){constresult=awaitstartOAuthLogin({
providerId,redirectUri: `${window.location.origin}/oauth/callback`,returnTo: `${window.location.origin}/dashboard`,});window.location.assign(result.authorizationUrl);}return(<div>{providers.map(provider=>(<buttonkey={provider.id}onClick={()=>voidsignIn(provider.id)}>
Continue with {provider.name}</button>))}</div>);}Create a callback route that reads the provider query params and asks Seamless Auth to complete the login:
import{useEffect}from'react';import{useAuth}from'@seamless-auth/react';functionOAuthCallback(){const{ finishOAuthLogin }=useAuth();useEffect(()=>{constparams=newURLSearchParams(window.location.search);// Persist the provider you passed to startOAuthLogin so the callback knows// which provider to finish. The built-in AuthRoutes flow stores this in// sessionStorage; use whatever your custom start flow saved.constproviderId=sessionStorage.getItem('seamless:oauth:provider');constcode=params.get('code');conststate=params.get('state');if(!providerId||!code||!state){return;}voidfinishOAuthLogin({ providerId, code, state }).then(()=>{window.location.assign('/dashboard');});},[finishOAuthLogin]);return<p>Finishing sign-in...</p>;}Some callback failures are the user's to fix, so the API returns a stable code alongside the error
message. getOAuthErrorCode() narrows it to the codes this SDK knows about and returns undefined
for everything else, so unexpected failures keep your generic message:
import{getOAuthErrorCode,useAuth}from'@seamless-auth/react';const{ error }=awaitfinishOAuthLogin({ providerId, code, state });switch(getOAuthErrorCode(error)){case'oauth_missing_email':
// The provider account shared no email address.break;case'oauth_email_not_verified':
// The provider account's email is unverified.break;case'oauth_missing_subject':
// The provider returned no usable account identifier.break;default:
// No error, or one without a recognized code.break;}The bundled AuthRoutes callback screen already maps these three codes to actionable text.
For fully custom UI without useAuth(), call the headless client directly:
constproviders=awaitauthClient.listOAuthProviders();conststarted=awaitauthClient.startOAuthLogin({providerId: providers.providers[0].id,redirectUri: `${window.location.origin}/oauth/callback`,});window.location.assign(started.authorizationUrl);OAuth must be enabled on the Seamless Auth API with LOGIN_METHODS including oauth and at least
one configured oauth_providers entry. Provider client secrets live on the server and are referenced
by environment variable name; they are never passed through this SDK.
For production providers, configure exact redirectUris on the Seamless Auth API. The SDK should
send the callback URL it expects to receive, but redirect allowlisting, signed state expiry, OIDC
nonce handling, email verification policy, and account-linking policy are enforced by the API.
The built-in views avoid logging OTPs, magic-link tokens, PRF salts, or raw exception payloads that may contain sensitive request URLs.
For custom auth UIs, use the exported client directly:
import{createSeamlessAuthClient}from'@seamless-auth/react';constauthClient=createSeamlessAuthClient({apiHost: 'https://your.api',});const{ data, error }=awaitauthClient.login({identifier: 'user@example.com',passkeyAvailable: true,});if(error){// error.message, error.status, and error.body carry the server detailreturn;}// data is typed as LoginStartResultconsole.log(data.loginMethods);The headless client exposes helpers for:
- current-user/session lookup
- login and passkey login
- registration
- phone OTP and email OTP
- magic-link request, verify, and polling
- OAuth provider listing, start, and callback completion
- passkey registration
- step-up status, passkey verification, and TOTP verification
- TOTP enrollment, status, and disable
- logout and delete-user
- credential update and deletion
The request and response types are aliases of
@seamless-auth/types, which is generated from
the auth API's schemas. User, Credential, Organization, StepUpStatus, MessageResult, and the
other wire shapes describe what the API actually sends, rather than a second copy maintained here that
could drift from it.
The dependency is types-only. Nothing from it is imported at runtime, so no schema validation library
reaches your bundle. Names exported from this package stay the SDK's own, so you keep importing
Credential from @seamless-auth/react.
Two SDK concerns are deliberately not shared, because they are not wire contracts: the PRF helper
types and the SeamlessAuthResult wrapper.
Every request method resolves to a SeamlessAuthResult<T>:
typeSeamlessAuthResult<T>=|{data: T;error: null}|{data: null;error: SeamlessAuthError};Check error first, then read data. TypeScript enforces this: data is not readable until the
error has been ruled out.
const{ data, error }=awaitauthClient.getCurrentUser();if(error){console.log(error.message,error.status,error.body);return;}setUser(data.user);// typed as CurrentUserResultNothing throws for an HTTP failure, and transport failures are absorbed too, reported as an error
with status0. That means an expected auth outcome such as a wrong OTP, an expired magic link, or
a disabled provider is a value you can map straight to UI state rather than an exception to catch.
SeamlessAuthError carries the server's message, the HTTP status, and the parsed response
body, so you can branch on a specific failure.
A passkey or step-up ceremony can fail in the browser before any request is sent, so those results
carry the thrown error as cause with status0. Use getWebAuthnErrorDetail() to read it: the
name is the DOMException name that separates the cases a user can act on, and code is
SimpleWebAuthn's narrower reason when it identified one.
import{getWebAuthnErrorDetail}from'@seamless-auth/react';const{ error }=awaitauthClient.verifyStepUpWithPasskey();constdetail=getWebAuthnErrorDetail(error);switch(detail?.name){case'NotAllowedError':
// The prompt was dismissed, or the account has no passkey to assert.break;case'SecurityError':
// The origin or RP ID does not match what the API is configured for.break;case'InvalidStateError':
// This authenticator already holds a passkey for the account.break;default:
// Not a ceremony failure. Fall back to error?.message.break;}getWebAuthnErrorDetail() returns undefined for any error that did not come from a ceremony, so an
HTTP failure keeps flowing through error.message and error.body as usual.
The single exception is isPasskeySupported-style capability checks:
isPasskeyPrfSupported(): Promise<boolean> is a local check rather than a request, so it returns a
plain boolean.
If you want custom React screens but do not want to manually recreate the client, use the exported hooks:
import{useAuth,useAuthClient,usePasskeySupport}from'@seamless-auth/react';functionCustomLogin(){const{ refreshSession }=useAuth();constauthClient=useAuthClient();const{ passkeySupported, loading }=usePasskeySupport();asyncfunctionhandleEmailLogin(){const{ error }=awaitauthClient.login({identifier: 'user@example.com',passkeyAvailable: passkeySupported,});if(!error){awaitrefreshSession();}}return(<buttondisabled={loading}onClick={()=>voidhandleEmailLogin()}>
Sign in
</button>);}useAuth() helpers and the headless client report failure the same way: both return
{ data, error } and neither throws. Whatever surface you reach for, the handling is identical.
const{ error }=awaitupdateCredential({ ...credential,friendlyName: 'Work laptop'});if(error){setMessage(error.message);}Helpers that also mutate provider state, such as switchOrganization and deleteCredential, apply
that state change only when the call succeeds, then hand the result back for you to inspect.
Worked examples for the flows the bundled screens cover, using only public primitives.
Registration is two steps: create the account, then verify the emailed code. Call markSignedIn()
once the account is live so returning visits can default to sign-in.
import{useAuth,useAuthClient}from'@seamless-auth/react';import{useState}from'react';functionCustomRegistration(){const{ markSignedIn, refreshSession }=useAuth();constauthClient=useAuthClient();const[step,setStep]=useState<'details'|'verify'>('details');const[message,setMessage]=useState('');asyncfunctioncreateAccount(email: string){// Registration needs only an email. A phone can be added and verified later.const{ error }=awaitauthClient.register({ email });if(error){setMessage(error.message);return;}// The API emails a verification code as part of registering.setStep('verify');}asyncfunctionverifyCode(code: string){const{ error }=awaitauthClient.verifyEmailOtp(code);if(error){setMessage(error.message);return;}markSignedIn();awaitrefreshSession();}returnstep==='details' ? (<DetailsFormonSubmit={createAccount}error={message}/>) : (<CodeFormonSubmit={verifyCode}onResend={()=>authClient.requestEmailOtp()}error={message}/>);}To offer a passkey right after registering, call registerPasskey() before refreshSession():
const{ data, error }=awaitauthClient.registerPasskey({friendlyName: 'My laptop',platform: 'macOS',browser: 'Chrome',deviceInfo: navigator.userAgent,});if(!error){console.log(data.credentialId,data.prfCapable);}The request helpers take no identifier.
requestMagicLink(),requestLoginEmailOtp(), andrequestLoginPhoneOtp()send nothing but the session cookie. They rely on server-side state established by a precedinglogin()call, so calling them without it fails or targets the wrong account. This is not obvious from their signatures. Always calllogin()first, and use the same browser session for the continuation step.
functionCustomLoginContinuation(){const{ login, refreshSession }=useAuth();constauthClient=useAuthClient();asyncfunctionstart(identifier: string){// Required first: this is what the request helpers below depend on.const{ data, error }=awaitlogin(identifier,false);if(error){return;}// Offer only what the server says this account supports.returndata.loginMethods??['magic_link','email_otp'];}asyncfunctionsendEmailCode(){const{ error }=awaitauthClient.requestLoginEmailOtp();if(error){// surface error.message}}asyncfunctionsubmitEmailCode(code: string){const{ error }=awaitauthClient.verifyLoginEmailOtp(code);if(!error){awaitrefreshSession();}}}Magic links complete in whichever tab opens the emailed link, so a custom flow needs two pieces.
The waiting screen polls until the link is used:
constinterval=setInterval(async()=>{const{ error }=awaitauthClient.checkMagicLink();if(!error){clearInterval(interval);awaitrefreshSession();}},5000);The landing route verifies the token from the query string, then refreshes its own session:
functionCustomMagicLinkLanding(){const{ refreshSession }=useAuth();// plus: import { useEffect } from 'react'constauthClient=useAuthClient();useEffect(()=>{consttoken=newURLSearchParams(window.location.search).get('token');if(!token)return;voidauthClient.verifyMagicLink(token).then(async({ error })=>{if(!error){// Refresh here too. This tab set the cookie, but its provider state// was loaded before the cookie existed.awaitrefreshSession();}});},[authClient,refreshSession]);return<p>Finishing sign-in...</p>;}The auth API emails a link pointing at /verify-magiclink?token=..., so a custom app must serve that
path.
useAuth() exposes the signed-in user's passkeys plus helpers to rename and remove them. These
helpers update provider state on success and report failure through error.
import{useAuth}from'@seamless-auth/react';importtype{Credential}from'@seamless-auth/react';import{useState}from'react';functionPasskeyList(){const{ credentials, updateCredential, deleteCredential }=useAuth();const[message,setMessage]=useState('');asyncfunctionrename(credential: Credential,friendlyName: string){const{ error }=awaitupdateCredential({ ...credential, friendlyName });if(error){setMessage(error.message);}}asyncfunctionremove(credentialId: string){const{ error }=awaitdeleteCredential(credentialId);if(error){setMessage(error.message);}}return(<ul>{credentials.map(credential=>(<likey={credential.id}>{credential.friendlyName??credential.deviceInfo}<buttononClick={()=>voidrename(credential,'Work laptop')}>Rename</button><buttononClick={()=>voidremove(credential.id)}>Remove</button></li>))}</ul>);}Credential.lastUsedAt and Credential.createdAt are ISO 8601 strings, which is what the API sends.
Wrap them yourself to format:
constlastUsed=credential.lastUsedAt ? newDate(credential.lastUsedAt) : null;Removing a passkey is a sensitive change. Gate it behind a fresh step-up when the account has other
factors, using refreshStepUpStatus() and verifyStepUpWithPasskey() from the step-up section.
AuthRoutes serves these canonical paths:
/login/passkey-login/verify-phone-otp/verify-email-otp/verify-magic-link/oauth/callback/register-passkey/magic-link-sent
These are optional UI wrappers over the same SDK primitives the package now exports for custom flows.
The earlier mixed-case paths were renamed and are no longer served. Anything linking directly to
them now falls through to /login, so update those links:
| Old path | New path |
|---|---|
/passKeyLogin | /passkey-login |
/verifyPhoneOTP | /verify-phone-otp |
/verifyEmailOTP | /verify-email-otp |
/registerPasskey | /register-passkey |
/magiclinks-sent | /magic-link-sent |
Two paths are unchanged because they are owned by contracts outside this package:
/verify-magiclinkis the URL the auth API builds when it emails a magic link, so it has to match that value exactly. Renaming it here would send every emailed link to/loginwith the token discarded./oauth/callbackis registered with OAuth providers as an allowed redirect URI, so renaming it would break configured integrations.
This package assumes a Seamless Auth-compatible backend with the auth adapter mounted at /auth.
- Requests target
${apiHost}/auth/... apiHostmay be provided with or without a trailing slash- Requests are sent with
credentials: 'include' AuthProvidervalidates the current session by calling/users/meon load
The built-in flows assume compatible endpoints for:
/loginDELETE /logoutfor the current sessionDELETE /logout/allfor every session owned by the current user/registration/register/webAuthn/login/start/webAuthn/login/finish/webAuthn/register/start/webAuthn/register/finishPOST /otp/generate-phone-otpPOST /otp/generate-email-otp/otp/verify-phone-otp/otp/verify-email-otpPOST /otp/generate-login-phone-otpPOST /otp/generate-login-email-otp/otp/verify-login-phone-otp/otp/verify-login-email-otpPOST /magic-link/magic-link/check/magic-link/verify/:token/oauth/providers/oauth/:providerId/start/oauth/:providerId/callback/step-up/status/step-up/webauthn/start/step-up/webauthn/finish/totp/status/totp/enroll/start/totp/enroll/verify/totp/disable/totp/verify-mfa/users/me/users/credentials/users/delete/organizations/organizations/:organizationId/organizations/:organizationId/switch/organizations/:organizationId/members/organizations/:organizationId/members/:userId
The state-changing OTP and magic-link request routes are POST (marked above). They were previously
GET, which made them reachable as simple cross-site requests, so an <img> tag could trigger SMS or
email sends to a signed-in user. Using @seamless-auth/react with an older adapter that only serves the
GET forms returns a 404 for those requests. See the changelog for the minimum adapter version.
- This package does not create its own
<BrowserRouter>. - It is designed to fit into your app’s existing routing tree.
- The quickest path is
AuthProvider+AuthRoutes. - The most flexible path is
AuthProvider+ custom UI usinguseAuth(),useAuthClient(), andusePasskeySupport().
AGPL-3.0-only