npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rhinestone/wallet

v0.14.0

Published

Rhinestone Wallet SDK for passkey-backed smart accounts

Downloads

524

Readme

@rhinestone/wallet

Rhinestone Wallet SDK for passkey-backed smart accounts and cross-chain intents.

Passkey-based authentication SDK for Web3 applications. Build seamless, secure authentication flows using passkeys (WebAuthn) for smart accounts.

Installation

npm install @rhinestone/wallet
# or
pnpm add @rhinestone/wallet
# or
yarn add @rhinestone/wallet

Peer Dependencies

npm install viem @wagmi/core

Entry Points

The SDK provides multiple entry points for different use cases:

| Entry Point | Import | Use Case | |-------------|--------|----------| | Default | @rhinestone/wallet | Core client, provider, registry, types | | Server | @rhinestone/wallet/server | JWT sponsorship signers for app-sponsored intents | | Wagmi | @rhinestone/wallet/wagmi | Wagmi connector integration | | Headless | @rhinestone/wallet/headless | Headless client without dialog UI |

Quick Start

Basic Setup

import { WalletClient, createWalletProvider } from '@rhinestone/wallet';

// Create the client
const client = new WalletClient({
  providerUrl: 'https://passkey.1auth.app',
  clientId: 'my-app',
});

// Create an EIP-1193 compatible provider (chainId is required)
const provider = createWalletProvider({ client, chainId: 8453 }); // Base

Wagmi Connector

import { rhinestoneWallet } from '@rhinestone/wallet/wagmi';
import { WalletClient } from '@rhinestone/wallet';
import { createConfig, http } from 'wagmi';
import { mainnet, sepolia } from 'wagmi/chains';

// The connector wraps a configured WalletClient.
const client = new WalletClient({ providerUrl: 'https://passkey.1auth.app', clientId: 'my-app' });

const config = createConfig({
  chains: [mainnet, sepolia],
  connectors: [
    rhinestoneWallet({ client }),
  ],
  transports: {
    [mainnet.id]: http(),
    [sepolia.id]: http(),
  },
});

Server-Side JWT Sponsorship

import { createSponsorshipSigner } from '@rhinestone/wallet/server';

// Reads RHINESTONE_JWT_PRIVATE_KEY / _INTEGRATOR_ID / _PROJECT_ID / _APP_ID / _KEY_ID
// from process.env. Pass `{ credentials }` to supply them explicitly instead.
const signer = createSponsorshipSigner();

// Use in your API routes
const accessToken = await signer.accessToken();
const extensionToken = await signer.extensionToken(intentOp); // accepts the JSON-stringified intentOp

Core Exports

@rhinestone/wallet

  • WalletClient - Main client for passkey operations
  • createWalletProvider() - EIP-1193 compatible provider factory
  • createPasskeyWalletClient() - viem WalletClient with passkey support
  • definePermissions(), createCrossChainPermission() - Session-key permission helpers
  • WalletClient.grantPermissions(), listSessionGrants(), revokeSessionGrant({ grantId, accountAddress }) - Grant, list and revoke SmartSession permissions; a revoke is signed in the sign dialog as one batch across the grant's chains
  • getSupportedChainIds(), getSupportedChains(), getChainById() - Async, orchestrator-backed chain discovery
  • hashMessage(), verifyMessageHash() - Message verification
  • Type exports for intents, accounts, sponsorship, and more

Chain discovery is lazy and asynchronous:

const chainIds = await getSupportedChainIds()
const chains = await getSupportedChains({ includeTestnets: true })

Token catalogs and symbol resolution are intentionally not part of the SDK. Pass token addresses directly to intent and cross-chain permission inputs.

@rhinestone/wallet/server

  • createSponsorshipSigner() - Recommended. Framework-agnostic signer that reads credentials from env and exposes accessToken() / extensionToken(intentOp)
  • verifyWalletAccount() - Server-side check that an address belongs to a known 1auth account
  • hashMessage(), verifyMessageHash(), encodeWebAuthnSignature() - Verification / signing utilities
  • JwtCredentials, SponsorshipSigner - Configuration types

@rhinestone/wallet/wagmi

  • rhinestoneWallet() - Wagmi connector factory

App-Sponsored Intents

What the JWT is for

Every user intent and asset read carries your app's JWT, so sponsorship is required: without it, sendIntent, sendBatchIntent and getAssets fail before reaching the network. You mint a short-lived JWT, signed with a private key Rhinestone issued to your project, and the orchestrator charges your account for any intent that JWT authorizes.

The JWT is purely a billing authorization — it says "this app vouches for paying this intent." It does not authenticate the user or authorize the transaction itself; that's the passkey/WebAuthn signature, which is independent. Sponsorship and signing happen in parallel.

The private key must never reach the browser, so the tokens are always minted server-side:

  1. Host the signer. Run createSponsorshipSigner() from @rhinestone/wallet/server behind two endpoints on your own backend, gated by your existing session auth. This is where you enforce per-user policy — spending limits, allowlists, the shouldSponsor filter. You decide exactly when and for whom a token is minted.

  2. Point the client at it. Pass those two endpoint URLs as sponsorship and the SDK handles fetching, caching and expiry for you.

How It Works

sequenceDiagram
    participant App as Your App
    participant BE as Your Backend
    participant SDK as @rhinestone/wallet
    participant PS as 1auth Passkey Service

    Note over App,SDK: 1. Configure the client with sponsorship URLs
    App->>SDK: new WalletClient({<br/>  sponsorship: {<br/>    accessTokenUrl,<br/>    extensionTokenUrl<br/>  }<br/>})

    Note over SDK,BE: 2. Prepare the intent
    SDK->>PS: POST /api/intent/prepare
    PS-->>SDK: { intentOp, digestResult, ... }

    Note over SDK,BE: 3. In parallel: fetch tokens (same-origin, with session cookie) + WebAuthn ceremony
    par
      SDK->>BE: GET /sponsorship/access-token
      BE-->>SDK: { token: "eyJhbG..." }
    and
      SDK->>BE: POST /sponsorship/extension-token { intentOp }
      BE-->>SDK: { token: "eyJhbG..." }
    and
      SDK->>SDK: WebAuthn signing ceremony
    end

    Note over SDK,PS: 4. Execute with pre-fetched tokens in request body
    SDK->>PS: POST /api/intent/execute<br/>{ signature, sponsorship: { accessToken, extensionToken } }
    PS-->>SDK: { intentId }

The two tokens

App sponsorship is authorized by two short-lived JWTs your backend mints with createSponsorshipSigner(). Both are signed with your Rhinestone JWK — ES256 for an EC key, RS256 for RSA — and carry your integratorId (issuer), projectId (subject), and appId.

| Token | typ | TTL | Purpose | |-------|-------|-----|---------| | Access token | access | 1 hour | Identifies your app to the orchestrator. Not bound to any intent, so it's safe to cache and reuse across many intents (the SDK does exactly this). | | Extension token | intent_extension | 5 minutes | Authorizes sponsorship of one specific intent. Minted fresh per intent. |

The extension token is what makes sponsorship safe to expose to the browser: its policy.sponsorship.intent_input.digest claim is a JCS-canonicalized (RFC 8785) SHA-256 hash of the exact intentOp being signed. The orchestrator recomputes that digest from the intent it receives and rejects the token if they differ — so a token minted for "send $5 to Alice" cannot be replayed to authorize "send $5000 to Mallory". Each extension token also carries a unique jti.

Because the digest pins the token to one intent, your backend's extension-token endpoint is the natural place to enforce per-user policy (spending limits, allowlists) — see the shouldSponsor filter on createSponsorshipSigner, which can veto by chain, account, or calls before a token is ever minted.

What the tokens actually contain

If you decode the JWTs your backend mints (e.g. paste into jwt.io), this is the shape. Both share the same header and iss/sub/aud claims — only typ, lifetime, and the intent-binding differ.

// Header (both tokens) — alg is ES256 for an EC key, RS256 for RSA
{ "alg": "ES256", "kid": "<keyId>" }

// Access token payload — app identity, reusable for 1h
{
  "typ": "access",
  "app_id": "<appId>",
  "iss": "<integratorId>",     // setIssuer
  "sub": "<projectId>",        // setSubject
  "aud": "rhinestone-api",     // override via JwtCredentials.audience
  "iat": 1719300000,
  "exp": 1719303600            // iat + 1h
}

// Extension token payload — authorizes ONE intent, expires in 5m
{
  "typ": "intent_extension",
  "app_id": "<appId>",
  "jti": "f47ac10b-58cc-4372-a567-0e02b2c3d479",  // unique per token
  "policy": {
    "sponsorship": {
      "scope": "intent",
      "intent_input": {
        // JCS-canonicalized SHA-256 of the exact intentOp; the orchestrator
        // recomputes this and rejects the token if it doesn't match.
        "digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
      }
    }
  },
  "iss": "<integratorId>",
  "sub": "<projectId>",
  "aud": "rhinestone-api",
  "iat": 1719300000,
  "exp": 1719300300            // iat + 5m
}

Token lifecycle & caching

The access token is reusable, so re-minting it on every intent is wasteful — but serving a stale one is worse. The SDK holds the access token in memory and only reuses it while its exp is comfortably in the future:

  • A 60-second safety margin (ACCESS_TOKEN_EXPIRY_MARGIN_MS) is subtracted from exp before deciding a cached token is still usable. This absorbs clock skew between your app server and the orchestrator's verifier plus the in-flight time of the prepare call — a token that looks valid locally can still be rejected downstream if it expires mid-request.
  • A token that can't be decoded, or carries no numeric exp, is treated as unusable so a malformed cache entry never gets served indefinitely.
  • If the extension-token endpoint returns 401, the cached access token is dropped immediately, so the next call re-mints rather than retrying with a token the orchestrator just rejected.
  • The extension token is never cached — it binds to a specific intentOp, so a fresh one is minted per intent.

Both fetches use credentials: "include" so your app's own session cookie authenticates the user; no separate auth channel is needed.

Troubleshooting

| Symptom | Likely cause | Fix | |---------|--------------|-----| | "Failed to get quote from orchestrator" (a 401 surfacing as a quote failure) | The access token's exp lapsed before the orchestrator verified it ("exp" claim timestamp check failed) — usually a long-lived tab serving a stale cached token, or large clock skew | Pass the endpoint URLs as sponsorship (the SDK re-mints with the 60s margin); check server clock sync | | 401 from your own access-token / extension-token endpoint | Your session guard rejected the request (user not logged in, cookie not sent cross-origin) | Ensure the endpoints are same-origin with your app and that the client sends credentials; gate with your existing session auth | | Sponsorship silently denied / SponsorshipDeniedError | A shouldSponsor filter vetoed the intent (chain/account/calls not allowed) | Confirm the intent matches your policy, or relax the filter | | Orchestrator rejects the extension token despite a valid signature | intent_input.digest doesn't match — the intentOp was mutated between prepare and execute, or you minted the token from a different object than the SDK sent | Mint the extension token from the exact intentOp string the SDK posts to your endpoint; don't re-serialize or reorder it | | Unsupported JWK kty / Unsupported EC curve at startup | The private key JWK isn't an EC (P-256/P-384/P-521) or RSA key | Use the JWK Rhinestone issued; ES256 (EC P-256) is the common case |

Why this shape

The SDK calls your token endpoints from the browser, same-origin. Your app's session cookie naturally authenticates the request — no need to pass bearer tokens through the SDK. The passkey service never fetches from your backend on your behalf, so there's no SSRF surface and no CORS config to worry about.

Setup

1. Get a signing key from the Rhinestone dashboard

Create a Rhinestone app in the Rhinestone dashboard, then register a JWT signing key under API keys → JWT keys. The keypair is generated in your browser; the private half downloads as <key-id>.jwk.json. These values go in your server environment (never the browser):

| Variable | Description | |----------|-------------| | RHINESTONE_JWT_PRIVATE_KEY | The downloaded JWK, minified to one line (EC P-256 / ES256 recommended, RSA accepted) | | RHINESTONE_INTEGRATOR_ID | Integrator ID you set when registering the key — becomes the JWT iss | | RHINESTONE_PROJECT_ID | Project the app belongs to — becomes the JWT sub | | RHINESTONE_APP_ID | A label you pick — prod, staging, one per deployment. Becomes the JWT app_id claim | | RHINESTONE_KEY_ID | Key ID you set when registering the key — the kid header |

createSponsorshipSigner() reads all five automatically; pass { credentials: { ... } } to supply them explicitly instead.

2. Create two endpoints on your backend, on the same origin as your app

Gate these with your existing session auth — the user's cookie rides along automatically.

// GET /api/sponsorship/access-token
import { createSponsorshipSigner } from '@rhinestone/wallet/server';

const signer = createSponsorshipSigner();

export async function handleAccessToken(req, res) {
  const user = await verifySession(req); // your app's auth
  if (!user) return res.status(401).json({ error: 'Unauthorized' });
  const token = await signer.accessToken();
  res.json({ token });
}

// POST /api/sponsorship/extension-token
// Body: { intentOp: string }  — JSON-stringified intent operation
export async function handleExtensionToken(req, res) {
  const user = await verifySession(req);
  if (!user) return res.status(401).json({ error: 'Unauthorized' });

  const { intentOp } = req.body;

  // Enforce policies
  if (await exceedsSpendingLimit(user.id)) {
    return res.status(403).json({ error: 'Spending limit exceeded' });
  }

  const token = await signer.extensionToken(intentOp);
  res.json({ token });
}

3. Configure WalletClient with the endpoint URLs

import { WalletClient, createWalletProvider } from '@rhinestone/wallet';

const client = new WalletClient({
  providerUrl: 'https://passkey.1auth.app',
  clientId: 'my-app',
  sponsorship: {
    accessTokenUrl: '/api/sponsorship/access-token',
    extensionTokenUrl: '/api/sponsorship/extension-token',
  },
});

const provider = createWalletProvider({ client, chainId: 8453 });

const txHash = await provider.request({
  method: 'eth_sendTransaction',
  params: [{ to: '0x...', data: '0x...' }],
});

SponsorshipConfig

type SponsorshipConfig = {
  accessTokenUrl: string;     // GET  → { token }
  extensionTokenUrl: string;  // POST { intentOp: string } → { token }
};

The SDK pre-fetches both tokens in parallel with the WebAuthn signing ceremony and passes them to the passkey service via the execute request body. The passkey service never makes outbound requests to your backend.

License

MIT

Migrating from @rhinestone/1auth

The replacement package is not published yet. Until publication is completed, test integrations with the packed tarball produced by pnpm --filter @rhinestone/wallet pack. Once available, remove @rhinestone/1auth and install @rhinestone/wallet. Existing accounts, credentials, provider/RP domains, SDK storage keys, sponsorship contracts, and recovery formats are unchanged. The legacy package remains functional but is no longer maintained; this is not a security or account migration.

All entry points keep the same shape:

| Legacy | Replacement | | --- | --- | | @rhinestone/1auth | @rhinestone/wallet | | @rhinestone/1auth/wagmi | @rhinestone/wallet/wagmi | | @rhinestone/1auth/server | @rhinestone/wallet/server | | @rhinestone/1auth/headless | @rhinestone/wallet/headless |

Rename branded APIs as follows; unbranded APIs keep their names:

  • OneAuthClient / OneAuthClientConfig → WalletClient / WalletClientConfig
  • OneAuthProvider* / createOneAuthProvider → WalletProvider* / createWalletProvider
  • OneAuthConnection* / createOneAuthConnection → WalletConnection* / createWalletConnection
  • OneAuthSession, OneAuthExternalWallet*, OneAuthPasskeyConnectionSession, and OneAuthConnect* → the corresponding Wallet* names
  • OneAuthError*, OneAuthCandidateError, and OneAuthResultError → the corresponding Wallet* names
  • OneAuthTelemetry* → WalletTelemetry*
  • OneAuthHeadlessClient → WalletHeadlessClient
  • oneAuth / OneAuthConnectorOptions → rhinestoneWallet / WalletConnectorOptions
  • verifyOneAuthAccount, its options/result, and OneAuthAccountVerificationError → verifyWalletAccount, VerifyWalletAccountOptions, VerifiedWalletAccount, and WalletAccountVerificationError

The Wagmi connector now displays Rhinestone Wallet and uses connector ID rhinestone-wallet. Wagmi will not match persisted connector metadata from the legacy 1auth connector on the first load after upgrading. Users can reconnect through the app's wallet UI; subsequent connections use the new identity normally. SDK account and credential storage is unchanged, so reconnecting does not create or migrate an account.