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

@moon-x/react-sdk

v0.12.0

Published

React authentication hooks and components for MoonX

Readme

@moon-x/react-sdk

React SDK for MoonX — embedded wallets with passkey-protected MPC signing for Ethereum and Solana, drop-in auth UI flows, and headless wallet methods.

npm install @moon-x/react-sdk
# or
pnpm add @moon-x/react-sdk

Quick start

Wrap your app with MoonXProvider and import the CSS bundle once:

import { MoonXProvider, mainnet, sepolia } from "@moon-x/react-sdk";
import "@moon-x/react-sdk/style.css";

export default function App() {
  return (
    <MoonXProvider
      publishableKey={process.env.NEXT_PUBLIC_MOONX_PUBLISHABLE_KEY!}
      config={{
        appearance: {
          accentColor: "#6366f1",
          backgroundColor: "#ffffff",
        },
        // One unified list for EVM + Solana. A bare viem chain uses its
        // built-in RPC; wrap a chain to override it; Solana entries carry an id.
        chains: [
          mainnet,
          sepolia,
          { id: "solana:mainnet", rpcUrl: "https://api.mainnet-beta.solana.com" },
        ],
        defaultChain: "eip155:1", // CAIP-2 | numeric (1) | name alias ("eth:sepolia")
      }}
    >
      <YourApp />
    </MoonXProvider>
  );
}

Open the auth modal and read user state:

import { useMoonX } from "@moon-x/react-sdk";

function LoginButton() {
  const { ready, isAuthenticated, user, start, logout } = useMoonX();

  if (!ready) return null;
  if (isAuthenticated) {
    return <button onClick={() => logout()}>Sign out ({user?.id})</button>;
  }
  return <button onClick={() => start?.()}>Sign in</button>;
}

Sign a message with one of the user's wallets:

import { useWallets } from "@moon-x/react-sdk";
import { useSignMessage } from "@moon-x/react-sdk/ethereum";

function SignDemo() {
  const { wallets } = useWallets();
  const { signMessage } = useSignMessage();

  const onSign = async () => {
    const wallet = wallets.find((w) => w.wallet_type === "ethereum");
    if (!wallet) return;
    const { signature } = await signMessage({
      message: "Hello world",
      wallet,
      options: { uiOptions: { showWalletUI: true } }, // opens the modal
    });
    console.log(signature);
  };

  return <button onClick={onSign}>Sign</button>;
}

Configuration

MoonXProvider accepts publishableKey (required) plus a config object:

| Field | Type | Purpose | |---|---|---| | appearance | AuthAppearance | The single theming surface — branding (accentColor, backgroundColor, displayMode, logo, loginHeaderTitle, fontFamily) and all design tokens inline (colors, borderRadius, typography, card, backdrop, components, per-mode light / dark). See Theming. | | loginMethods | ("email" \| "google" \| "apple" \| "wallet")[] | Which auth methods to show in the modal. | | walletChainType | "ethereum" \| "solana" \| "ethereum-or-solana" | Which wallet type to create at signup. | | chains | ChainConfigItem[] | Unified RPC / chain config for both EVM and Solana. Each element is a bare viem Chain (use its built-in RPC), an { chain, rpcUrl?, wsUrl? } EVM entry (to override the RPC), or a { id, rpcUrl, wsUrl? } Solana entry. Common viem chains are re-exported by this package. See docs/rpc-configuration.md. | | defaultChain | string \| number | Which configured chain is the default. Referenced by alias, CAIP-2 id, or numeric chainId — e.g. "eth:base", "eip155:8453", 8453, "solana:mainnet". Defaults to the first entry in chains. | | walletConnect.projectId | string | WalletConnect v2 project ID. When set, the connect-wallet modal offers a WalletConnect (EVM) option (QR) so mobile / non-injected wallets can connect using the configured/default EVM chain. | | emailConfig, passkeyEnrollConfig, signMessageConfig, signTransactionConfig, sendTransactionConfig, exportKeyConfig | various | Per-flow UI overrides — titles, button text, etc. | | security | Record<string, never> | Reserved for future per-app security knobs. The previously-configurable assertionCacheTtlMs was removed in Phase 4 of the presence-token gating work — every sensitive op now does a fresh WebAuthn ceremony and mints scope-bound single-use JWTs via the iframe's internal orchestrator, so there is no parent-side cache left to configure. See Security below. |

Hooks

Authentication & user state

| Hook | What it does | |---|---| | useMoonX() | The big one. { ready, isAuthenticated, user, start, logout, setAppearance, getSessionTokens, refreshUser, ... } + every SDK method on the same instance. | | useUser() | Just { user, refreshUser }. Re-subscribes to user changes. | | useLoginWithEmail({ onComplete?, onError? }) | Headless email-OTP. Returns { state, sendCode, loginWithCode, reset }. state is a discriminated union: idle / sending / awaiting-code / verifying / complete / error. | | useLoginWithOAuth() | Google + Apple flows. Returns { state, loginWithOAuth, reset }. | | useLogout() | { logout } — also clears local storage + iframe session. |

Passkeys

| Hook | What it does | |---|---| | usePasskeyStatus() | { status, refresh }. status.passkeys lists the user's enrolled passkeys with provider labels (e.g. "1Password on Chrome"). | | useRegisterPasskey() | First-time passkey enrollment for a user who signed in via OTP/OAuth without one. | | useAddPasskey() | Add an additional passkey to an authenticated user. | | useRemovePasskey() | Remove a passkey by its credential ID. |

Wallets

| Hook | What it does | |---|---| | useWallets() | { wallets, loading } — both Ethereum and Solana, fetched once on mount. | | useCreateWallet() | Mint a new MPC wallet. Pass { walletType: "ethereum" \| "solana" }. | | useImportKey() | Two-mode: headless if you pass a key, modal-driven if you don't. | | useConnectWallet() | Prebuilt connect-wallet modal. Connects an external wallet client-side: injected EVM (EIP-6963), Solana (Wallet Standard), and EVM WalletConnect (QR, when walletConnect.projectId is set). Connect-only — mode defaults to "connect"; "signIn"/"link" (SIWE/SIWS authentication) are not implemented yet and reject rather than silently succeed. | | useExternalWallets({ chain? }) | Headless external-wallet connection: { wallets, connect, disconnect, connected, status, refresh }. Covers injected EVM + Solana Wallet Standard. For WalletConnect, use the modal or the standalone connectWalletConnect(). | | useConnectedExternalWallets() | The external wallets connected this session (shared app-wide) for a picker / account switcher; each exposes signMessage. | | useAttachOAuth() / useDetachOAuth() | Link / unlink an OAuth provider on an existing user. |

Per-chain signing — /ethereum and /solana subpaths

Chain-aware hooks live under subpaths so they don't pull in the other chain's adapters if you only use one:

import { useSignMessage, useSignTransaction, useSendTransaction } from "@moon-x/react-sdk/ethereum";
import { useSignMessage as useSignSolanaMessage } from "@moon-x/react-sdk/solana";

Ethereum hooks:

  • useSignMessage — EIP-191 personal_sign. Returns { signature: "0x..." }.
  • useSignTransaction — Signs an EIP-1559 tx. Returns { signature, serializedSigned, hash }.
  • useSignTypedData — EIP-712. Returns { signature: "0x..." }.
  • useSignHash — Raw ECDSA digest sign (Privy parity — secp256k1_sign).
  • useSign7702Authorization — EIP-7702 delegation auth.
  • useSendTransaction — Sign + broadcast. Accepts a per-call chain selector and an inline rpcUrl override (both fall back to provider config).
  • useGetBalance — Native token balance. Same per-call chain / rpcUrl options.

Solana hooks:

  • useSignMessage — Ed25519 signature.
  • useSignTransaction — Signs a serialized base58 tx. Returns { signedTransaction: Uint8Array }.
  • useSendTransaction — Sign + broadcast. Accepts a per-call chain selector and an inline rpcUrl override.
  • useGetBalance — Lamport balance. Same per-call chain / rpcUrl options.
  • useGetTokenAccounts — SPL token accounts by owner. Same per-call chain / rpcUrl options.

Every signing hook accepts options.uiOptions.showWalletUI to toggle modal vs headless mode. With UI, the user sees the message/transaction in a modal and the biometric prompt fires only on the Sign tap. Headless skips the modal entirely.

Every sensitive op now does a fresh WebAuthn ceremony per call — the previous per-call requireFreshAssertion?: boolean flag was removed from the param types since it has no behavior left to opt into. See Security below.

Security

Every sensitive operation (signMessage, signTransaction, signTypedData, signHash, sign7702Authorization, sendTransaction, createWallet, importKey, exportKey, addPasskey, removePasskey) drives this server-verified ceremony per call:

  1. Server-issued challenge. SDK posts to /auth/passkey/presence/begin, server inserts a row in app_user_passkey_challenges and returns the WebAuthn options.
  2. Fresh WebAuthn assertion. The parent runs navigator.credentials.get; the user does a biometric. The assertor strips response.userHandle from the server-bound payload (DEK hygiene) and surfaces the userHandle separately for the iframe's local AES-GCM unwrap.
  3. Scope-bound, single-use JWT mint. SDK posts to /auth/passkey/presence/verify with purpose: "internal" and the scope set this op needs (e.g. ["keyshare_read", "sign"]). Server consumes the challenge, verifies the signature against the credential's stored public key, mints one short-lived JWT per scope with a unique jti.
  4. Per-endpoint enforcement. Each scoped JWT carries the X-MoonX-Presence header on its matching gated endpoint. MoonX middleware pins the JWT's op claim to the endpoint, then burns the jti in platform.app_presence_jti_used via INSERT ... ON CONFLICT DO NOTHING. Replay of the same token is rejected as presence token already consumed. Tokens have a 30-second TTL.

What this closes: captured userHandle + session JWT no longer unlocks DEK material offline. Even with both, an attacker can't fetch wraps / keyshares / drive the co-signer without producing a fresh WebAuthn signature for each op — which requires the credential's private key in the user's authenticator. See apps/platform/docs/notes/passkeys/presence-tokens.md (in the backend repo) for the full threat model and scope matrix.

The previously-configurable security.assertionCacheTtlMs and per-call requireFreshAssertion were removed entirely when presence-token gating shipped — they no longer exist on the public TypeScript surface. Every op is always-fresh by construction.

Theming

All theming flows through a single appearance object. Set a few high-level branding fields (accentColor, backgroundColor, displayMode, logo, loginHeaderTitle, fontFamily) — the SDK derives a full palette from accentColor / backgroundColor — and reach for the inline design tokens (colors, borderRadius, typography, card, backdrop, components, per-mode light / dark) when you want finer control.

<MoonXProvider
  config={{
    appearance: {
      // Branding
      accentColor: "#6366f1",
      backgroundColor: "#0f172a",
      displayMode: "dark",            // "light" | "dark" | "auto"
      fontFamily: "Inter, sans-serif",
      logo: { src: "/logo.svg", srcDark: "/logo-dark.svg", height: 32 },

      // Design tokens (all inline on appearance)
      colors: { warning: "#f59e0b", info: "#3b82f6", link: "#6366f1", ring: "#6366f1" },
      borderRadius: { button: "9999px", card: "1rem", input: "0.5rem" }, // or "none" | "sm" | "md" | "lg"
      typography: {
        fontFamilyHeading: "Poppins, sans-serif",
        fontUrl: "https://fonts.googleapis.com/css2?family=Poppins&display=swap",
        letterSpacing: "0.01em",
        fontSize: { sm: "0.8125rem", lg: "1.0625rem", xl: "1.375rem" },
      },
      card: { shadow: "0 10px 25px -5px rgb(0 0 0 / 0.2)", padding: "1.5rem", maxWidth: "26rem" },
      backdrop: { color: "rgb(0 0 0 / 0.5)", blur: "8px" },

      // Per-mode overrides (merged on top of the base tokens for the resolved mode)
      dark: { colors: { background: "#0b0b0f" } },
    },
  }}
>

Update appearance at runtime — handy for a light/dark toggle:

const { setAppearance } = useMoonX();
setAppearance({ displayMode: colorScheme });

Merge order (low → high): base light/dark theme → accentColor / backgroundColor derivations → appearance tokens (colors, borderRadius, …) → per-mode appearance.light / appearance.dark. So an explicit appearance token always wins over the high-level derivations.

Token reference

All theming tokens emit --moonx-* CSS variables. The base palette/radius/font emit the core tokens (e.g. --moonx-color-*, --moonx-border-radius-*); the newer granular tokens each fall back to their base token (e.g. --moonx-radius-button--moonx-border-radius-md), so defaults are unchanged until you set one.

| Group | Tokens | CSS variables | |---|---|---| | colors | accent, accentForeground, background, background2/3, foreground, foreground2–4, border, success, error, … | --moonx-color-* | | colors | warning, info, link, ring (focus) | --moonx-color-* | | borderRadius | sm, md, lg, full | --moonx-border-radius-* | | borderRadius | button, card, input | --moonx-radius-* | | typography | fontFamilyHeading, letterSpacing, fontSize.{sm,lg,xl}, fontWeight.{medium,bold} | --moonx-font-*, --moonx-letter-spacing | | typography | fontUrl | injected <link rel="stylesheet"> | | card | padding, shadow, borderWidth, maxWidth | --moonx-card-* | | backdrop | color, blur | --moonx-backdrop-* |

Per-component overrides

For finer control, appearance.components styles individual surfaces — card, button, input — independently of the global tokens. button and input accept background, text, border (color), borderWidth, and radius; card accepts the same minus background (the card surface is the global backgroundColor). Anything you omit falls through to the global tokens / --moonx-* defaults. The header is styled via the typography tokens instead (appearance.typography.colorHeading, fontFamilyHeading, …).

<MoonXProvider
  config={{
    appearance: {
      components: {
        card:   { border: "#23233a", radius: "1rem" },
        button: { background: "#a78bfa", text: "#0b0b14", radius: "9999px" },
        input:  { background: "#15151f", border: "#23233a", borderWidth: "1px" },
      },
    },
  }}
>

Each field maps to a --moonx-<component>-* CSS variable (e.g. --moonx-card-bg, --moonx-button-border-color); radius reuses --moonx-radius-<component>. Overrides layer on top of the global tokens, so you can restyle just the card while everything else follows the theme.

EVM chain helpers

The package re-exports gas / RPC utilities that work with any viem Chain:

import {
  getEvmGasPrice,
  getEvmMaxPriorityFeePerGas,
  getEvmNonce,
  estimateEvmGas,
  estimateEvmGasReserve,
  getRpcUrl,
  getChainById,
  isChainSupported,
  validateChainConfig,
  // Unified-config resolution (also used internally by the hooks):
  resolveChainEntry,
  resolveRpcUrl,
  resolveWsUrl,
  normalizeChainKey,
  buildChainIndex,
  isEvmEntry,
} from "@moon-x/react-sdk";

Useful when you need a pre-flight gas reserve estimate, want to resolve a configured chain's RPC yourself, or are building a custom chain-picker.

React Native

For React Native apps, use @moon-x/react-native-sdk — same hook shape with a WebView-backed transport.

License

UNLICENSED. All rights reserved.