Overview
OneAuthClient is the core class of the 1auth SDK. It handles all interactions with the passkey authentication system including:
- User registration and authentication
- Transaction signing with multiple UX modes
- Cross-chain intent submission
- Token swaps
Constructor
import { OneAuthClient } from "@rhinestone/1auth";
const client = new OneAuthClient({
providerUrl: "https://passkey.1auth.app",
clientId: "my-app",
sponsorship: {
accessTokenUrl: "/api/sponsorship/access-token",
extensionTokenUrl: "/api/sponsorship/extension-token",
},
theme: {
mode: "dark",
accent: "#6366f1",
},
recovery: {
// Passkey recovery is attempted first. This controls only the fallback.
fallback: { type: "verified-identity" },
},
});Config Options
| Option | Type | Required | Description |
|---|---|---|---|
providerUrl | string | No | URL of the 1auth provider (defaults to https://passkey.1auth.app) |
clientId | string | For app sponsorship/developer features | Identifies app metadata for sponsorship, attribution, recovery guardians, and related features. It does not select the browser WebAuthn RP namespace. |
webauthn | { rpId?: string } | No | WebAuthn runs in your top-level page with the exact page hostname as RP ID unless rpId selects a shared namespace. A custom rpId requires clientId. |
sponsorship | SponsorshipConfig | For intents and assets | Endpoints (or callbacks) that mint your app's JWT. Required by sendIntent, sendBatchIntent, grantPermissions, and getAssets; setting an individual intent's sponsor flag to false skips its extension token, not the app access token. Each method reports missing or failed credentials through its own result/exception contract, detailed below. Get a signing key from the Rhinestone dashboard; see Fee Sponsorship. |
dialogUrl | string | No | URL for the dialog UI (defaults to providerUrl) |
storageKey | string | No | Provider session storage key cleared on disconnect (defaults to "1auth-user"). A storageKey supplied to createOneAuthProvider() or createOneAuthConnection() takes precedence for that bound client. |
theme | ThemeConfig | No | UI customization options |
experimental_clear_signing | boolean | No | Experimental. Show the 1auth signing review iframe before WebAuthn. Defaults to false; blind signing remains the established flow. |
testnets | boolean | No | When true, chain queries prefer testnets. wallet_getAssets always returns grouped mainnet and testnet balances. |
prewarm | boolean | No | When true, schedules a one-time prewarm() on an idle callback after construction. Off by default — prefer calling prewarm() on button hover/focus. |
recovery | AccountRecoveryConfig | No | Controls account creation when passkey-based recovery is unavailable. Defaults to an encrypted backup file. Options: backup-file, verified-identity, or block. |
telemetry | OneAuthTelemetryConfig | No | Bridges SDK events and trace context into the host app's observability setup. Enabled by default when configured; set enabled: false to disable it. |
onDisconnect | () => void | No | Called after the SDK clears its session when a trusted passkey dialog invalidates it, including deployment-forced logout. Use it to clear host-app in-memory authentication state. |
Account recovery fallback
Passkey-based recovery is always attempted first. Configure recovery.fallback
to decide what account creation does when the selected passkey provider cannot
support it:
const client = new OneAuthClient({
recovery: {
fallback: { type: "verified-identity" },
},
});backup-file— default; ask the user to save an encrypted recovery file.verified-identity— rely on the verified email or OAuth recovery guardian.block— refuse account creation unless passkey-based recovery succeeds.
Authentication Methods
authenticate
Authenticate using the verified identity's current account state. The server selects registration when no account exists, login when a usable passkey exists, and recovery when the account has no usable passkey or the authentication ceremony is rejected. Identity and recovery UI stay in 1auth, while WebAuthn runs in the top-level app page.
const result = await client.authenticate();
if (result.success) {
console.log("Address:", result.session.accountAddress); // typed `0x${string}`
}authenticate({ flow: "login" }) and authenticate({ flow: "create-account" }) pass the requested intent as a hint; the server remains authoritative for account-state classification.
getSession
Return the current session as an AuthResult. The stored bearer is verified with the passkey service, and a rejected session disconnects the client.
const session = await client.getSession();
if (session.success) {
console.log("Address:", session.session.accountAddress);
} else if (session.error.code === "APP_ORIGIN_SESSION_REQUIRED") {
await client.authenticate();
}Asset Methods
getAssets
Fetches a unified portfolio across mainnets and testnets by account address.
This uses your configured sponsorship.accessToken to send an app JWT to the
1auth provider before any balance data is returned.
const assets = await client.getAssets({
accountAddress: "0x1111111111111111111111111111111111111111",
});
console.log("All balances:", assets.balances);
console.log("Mainnet balances:", assets.mainnets.balances);
console.log("Testnet balances:", assets.testnets.balances);getAssets() is exception-based. Missing sponsorship, token callback failures,
and portfolio request failures reject with an Error; handle the call with
try/catch.
Signing Methods
signWithModal
Full-screen modal with transaction details:
const result = await client.signWithModal({
accountAddress: "0x...",
challenge: "0x...",
description: "Review and sign",
transaction: {
actions: [{ type: "custom", label: "Approve action" }],
},
});The 1auth iframe renders review, then the SDK runs WebAuthn on the exact top-level application origin and returns the verified signature. The flow requires an active session and correlates iframe approval by exact window source, provider origin, request ID, and per-modal nonce.
experimental_clear_signing
Set experimental_clear_signing: true to evaluate 1auth's visible review
iframe for supported signing flows. The user reviews the payload before the
passkey ceremony starts; the browser's WebAuthn prompt is still shown.
const client = new OneAuthClient({
providerUrl: "https://passkey.1auth.app",
clientId: "my-app",
experimental_clear_signing: true,
});
const result = await client.sendIntent({
accountAddress: "0x...",
targetChain: 8453,
calls: [{ to: "0x...", data: "0x..." }],
});Per-call options can override the client setting:
await client.sendIntent({
accountAddress: "0x...",
targetChain: 8453,
calls: [{ to: "0x...", data: "0x..." }],
experimental_clear_signing: false,
});The option applies to sendIntent and sendBatchIntent. signMessage,
signTypedData, and signWithModal always show the review before WebAuthn.
It does not apply to signup, login, recovery, or permission-grant dialogs. See
Signing for limitations and failure behavior.
Intent Execution
sendIntent
Submit cross-chain intents to the Rhinestone orchestrator.
const result = await client.sendIntent({
accountAddress: "0x...",
targetChain: 8453,
calls: [
{
to: "0x...",
data: "0x...",
value: parseEther("0.1"),
label: "Mint NFT", // Optional — shown in the sign dialog
sublabel: "0.10 USDC",
icon: "https://your-app.example/nft.svg", // Optional fallback icon
},
],
closeOn: "completed",
});
if (result.success) {
console.log("TX Hash:", result.transactionHash);
}sendIntent() is result-based for sponsorship failures. Missing configuration
returns success: false with error.code === 'MISSING_APP_CREDENTIALS'.
Access-token or extension-token failures return success: false with
error.code === 'SPONSORSHIP_FETCH_FAILED'; the visible dialog may first offer
its prepare retry flow.
sendBatchIntent() uses a different result shape. It reports top-level failures
through its own optional errorCode and error fields rather than a nested
error.code, and some paths populate neither — a failed access-token fetch
returns only success: false with empty results, surfacing the reason through
SDK telemetry instead. Missing sponsorship sets error. See
Troubleshooting for the full list and how to read it.
Each call in calls is an IntentCall and accepts four optional UI fields:
| Field | Description |
|---|---|
label | Primary line in the sign dialog action card (e.g. "Mint NFT") |
sublabel | Secondary line under the label (e.g. "0.10 USDC") |
icon | Fallback icon URL (SVG / square PNG / data: URL, ≤ 8 KB) shown when 1auth's built-in token registry can't resolve a logo. Built-in icons always win — USDC / ETH / MATIC etc. ignore icon. |
abi | Optional ABI used by the sign dialog to render an unverified human-readable decode of data (function name + args). Same trust model as label / sublabel / icon — the dialog renders the decoded preview behind an "Unverified" badge and always shows the raw to + selector alongside as ground truth. See Per-call ABIs. |
With Token Requests (Output-First)
Use tokenRequests to specify what tokens a target-chain call needs before it
executes. The orchestrator determines the optimal path to deliver them from the
user's assets across any chain.
import { encodeFunctionData, parseUnits } from "viem";
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const amount = parseUnits("100", 6);
const transferData = encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: ["0xRecipient...", amount],
});
const result = await client.sendIntent({
accountAddress: "0x...",
targetChain: 8453,
calls: [
{
to: USDC_BASE,
data: transferData,
label: "Send USDC",
sublabel: "100 USDC",
},
],
tokenRequests: [
{
token: USDC_BASE,
amount,
},
],
closeOn: "completed",
});This is ideal for app actions such as transfers, deposits, and purchases that
need an output token before execution. For a plain swap where the user only
receives the output token, call sendIntent with calls: [] rather than
adding placeholder calldata.
Pass waitForHash: true if you need a transaction hash. Otherwise, rely on intentId + getIntentStatus.
grantPermissions
Grant a scoped SmartSession permission to an app-owned session key. 1auth
opens the permission review, collects the user's passkey approval, and
submits the install/enable transaction. Your app sends only the public
sessionKeyAddress; keep the private key in your own signer.
import { definePermissions, OneAuthClient } from "@rhinestone/1auth";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { parseUnits } from "viem";
const oneAuth = new OneAuthClient({
providerUrl: "https://passkey.1auth.app",
clientId: "my-app",
sponsorship: {
accessTokenUrl: "/api/sponsorship/access-token",
extensionTokenUrl: "/api/sponsorship/extension-token",
},
});
const privateKey = generatePrivateKey();
const sessionKeyAddress = privateKeyToAccount(privateKey).address;
const now = Math.floor(Date.now() / 1000);
const validUntil = now + 24 * 60 * 60;
const permissions = definePermissions({
address: mUSD,
name: "mUSD",
abi: erc20Abi,
functions: {
mint: {
params: {
to: { condition: "equal", value: accountAddress },
amount: { condition: "equal", value: parseUnits("0.1", 6) },
},
},
},
});
const result = await oneAuth.grantPermissions({
accountAddress,
targetChains: [84532],
sessionKeyAddress,
validAfter: now,
validUntil,
maxUses: 25,
...permissions,
});grantPermissions() also reports sponsorship failures as structured results:
MISSING_APP_CREDENTIALS when sponsorship is not configured and
SPONSORSHIP_FETCH_FAILED when the access-token callback fails.
After this grant succeeds, the app can prepare and submit a matching
mUSD.mint(accountAddress, parseUnits('0.1', 6)) intent with its
session key. That later mint does not open another 1auth user-signature
dialog; the SmartSession validator enforces the to and amount
constraints on-chain.
For bridge or cross-chain swap sessions, include a bridge claim
permission with crossChainPermits. Selector permissions still describe
the destination-chain calls; crossChainPermits describes the
source-chain Permit2 claim that funds the route.
import {
createCrossChainPermission,
definePermissions,
} from "@rhinestone/1auth";
import { parseUnits } from "viem";
import { arbitrumSepolia, baseSepolia } from "viem/chains";
const ARB_SEPOLIA = 421614;
const BASE_SEPOLIA = 84532;
const amount = parseUnits("0.1", 6);
const now = Math.floor(Date.now() / 1000);
const validUntil = now + 86400;
const usdcOnArb = "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d";
const swap = definePermissions({
address: rwaSwap,
name: "RWASwap",
abi: rwaSwapAbi,
functions: {
swap: {
params: {
usdcIn: { condition: "equal", value: amount },
recipient: { condition: "equal", value: accountAddress },
},
},
},
});
const bridge = createCrossChainPermission({
from: { chain: baseSepolia, token: mUSDOnBase, maxAmount: amount },
to: { chain: arbitrumSepolia, token: usdcOnArb, recipient: accountAddress },
validAfter: BigInt(now),
validUntil: BigInt(validUntil),
settlementLayers: ["ACROSS"],
});
await oneAuth.grantPermissions({
accountAddress,
// Destination chain for swap; source chain for Permit2 claim.
targetChains: [ARB_SEPOLIA],
sourceChains: [BASE_SEPOLIA],
sessionKeyAddress,
validAfter: now,
validUntil,
maxUses: 25,
permissions: swap.permissions,
crossChainPermits: [bridge],
contracts: swap.contracts,
});The source chain must be in sourceChains. During settlement, Permit2
verifies the claim by calling isValidSignature on the source-chain
account, so the SmartSession validator must be installed and enabled
there too.
Plain swaps
Plain swaps use sendIntent with an empty calls array and one or more
tokenRequests:
import { parseUnits } from "viem";
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const result = await client.sendIntent({
accountAddress: "0x...",
targetChain: 8453,
calls: [],
tokenRequests: [
{
token: USDC_BASE,
amount: parseUnits("100", 6),
},
],
sourceAssets: ["0x0000000000000000000000000000000000000000"],
});For routes that fund target-chain execution, use sendIntent or
walletClient.sendCalls with real calls plus explicit
tokenRequests. Do not pass dummy data: '0x' calls or self-transfers just to
make an intent shape fit.
Utility Methods
getIntentStatus
Poll for transaction completion:
const status = await client.getIntentStatus(intentId);
console.log(status.status); // 'pending' | 'completed' | 'failed'setTheme
Update theme at runtime:
client.setTheme({
accent: "#10b981",
mode: "dark",
});prewarm
Load the dialog ahead of the first user interaction so it opens instantly. The SDK already pre-connects (DNS/TLS) at construction; prewarm() goes further and loads the dialog bundle + font into a hidden iframe, so when the user opens a real dialog it paints from cache instead of doing a cold load.
Call it on a likely-intent signal — hover or focus of your sign-in / pay button — so pages where the user never authenticates don't pay for the dialog iframe:
<button
onClick={() => client.authenticate()}
onPointerEnter={() => client.prewarm()}
onFocus={() => client.prewarm()}
>
Sign in
</button>prewarm() is idempotent and best-effort: it never throws and never blocks a real open (a failed warm just falls back to a normal load). It resolves true once the dialog bundle has loaded. It loads a dedicated, side-effect-free warm route (it runs no auth flow and creates no backend state), which still warms the shared bundle every dialog flow reuses. Call client.destroyPrewarm() to release the hidden iframe when auth is no longer likely on the current view.
Error handling
SDK methods preserve machine-readable codes and provider diagnostics for signing, intent, simulation, sponsorship, and network failures.
const result = await client.sendIntent({ ... })
if (!result.success) {
console.error(result.error?.code, result.error?.details)
}See Error Codes and Diagnostics for the complete code reference,
OneAuthError usage, simulation links, trace IDs, and recommended application
behavior.
Supported Networks
1auth supports all chains in the Rhinestone orchestrator network. Query supported chains at runtime:
import { getSupportedChainIds, getSupportedChains } from "@rhinestone/1auth";
// Catalog IDs include every orchestrator-supported EVM chain.
const chainIds = await getSupportedChainIds();
// viem Chain objects are returned when the installed viem version defines them.
const chains = await getSupportedChains();
// Include testnets or restrict discovery to an explicit allowlist.
const testnets = await getSupportedChains({ includeTestnets: true });
const selected = await getSupportedChains({ chainIds: [1, 8453] });Mainnet chains include Ethereum, Base, Arbitrum, Optimism, Polygon, and others. The targetChain parameter in sendIntent() accepts any supported chain ID.
Transaction Lifecycle
When you call sendIntent(), the transaction progresses through these stages:
| Status | Description |
|---|---|
pending | Intent created, waiting for quote |
quoted | Quote received from orchestrator |
signed | User has signed with their passkey |
submitted | Submitted to the Rhinestone orchestrator |
claimed | A solver has claimed the intent |
preconfirmed | Pre-confirmation received (typically < 1 second) |
filled | Transaction filled on the target chain |
completed | Fully confirmed on-chain |
failed | Intent failed |
expired | Intent expired before execution |
The closeOn option controls when sendIntent() resolves:
"preconfirmed"(default) — Resolves quickly, recommended for most use cases"claimed"— Resolves at first solver claim (fastest, less certain)"filled"— Resolves when the transaction hits the target chain"completed"— Waits for full on-chain confirmation (slowest)
For a transaction hash, pass waitForHash: true or poll with getIntentStatus() after the intent resolves.
Notes
- Create one client instance and reuse it
- Use the
closeOnparameter to control when promises resolve - The client handles identity verification and WebAuthn orchestration internally.
- Browser credentials and accounts are isolated by the exact hostname unless a registered shared RP ID and exact server-authorized
rpOriginsare configured. The SDK sends onlyrpId; origins are never client-owned configuration.