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

@rockfridrich/villa-sdk

v0.1.3

Published

Villa Identity SDK - Privacy-first passkey authentication for Base network

Readme

@rockfridrich/villa-sdk

Privacy-first passkey authentication for Base network. No wallets. No passwords. Just Face ID.

npm version npm downloads License: MIT TypeScript

Security & Trust

| Guarantee | Implementation | | ------------------------------- | ----------------------------------- | | Passkeys never leave device | WebAuthn hardware-bound keys | | Zero knowledge | Villa never sees your private key | | Trustless deploys | npm provenance attestation via OIDC | | Minimal dependencies | Only viem + zod (peer deps) | | Origin validation | Strict postMessage origin checks | | Input validation | Zod schemas for all external data |

Dependencies: 2 peer (viem, zod)
Bundle size: ~22KB minified

Quick Start

npm install @rockfridrich/villa-sdk viem zod

Simple API (Recommended)

import { villa } from "@rockfridrich/villa-sdk";

// One-liner sign in
const user = await villa.signIn();
console.log(user.address, user.nickname, user.avatar);

// Check auth state anywhere
if (villa.user) {
  console.log("Logged in as", villa.user.nickname);
}

// Sign out
villa.signOut();

// Listen to auth changes
villa.onAuthChange((user) => {
  console.log("Auth changed:", user?.nickname || "signed out");
});

Advanced API

For apps needing custom configuration:

import { Villa } from "@rockfridrich/villa-sdk";

const client = new Villa({
  appId: "your-app-id", // Optional - for analytics
  network: "base-sepolia", // Use testnet
});

const result = await client.signIn();

if (result.success) {
  console.log("Welcome", result.identity.nickname);
} else {
  console.error("Auth failed:", result.error, result.code);
}

API Reference

Simple API (villa)

Zero-config singleton for most use cases:

import { villa } from "@rockfridrich/villa-sdk";

villa.signIn(); // Returns Promise<VillaUser>
villa.signOut(); // Clears session
villa.user; // Current user or null
villa.onAuthChange(); // Subscribe to auth changes
villa.config({}); // Override settings if needed

Villa Client (Advanced)

For apps needing custom configuration:

Constructor

const client = new Villa(config);

Parameters:

  • config.appId (string, optional) - Application identifier for analytics
  • config.network (string, optional) - 'base' (default) or 'base-sepolia'
  • config.apiUrl (string, optional) - Override API endpoint

Methods

signIn(options?)

Opens fullscreen auth iframe and authenticates user.

const result = await villa.signIn({
  scopes: ["profile", "wallet"], // Optional: data to request
  onProgress: (step) => {
    // Optional: track progress
    console.log(step.message);
  },
  timeout: 5 * 60 * 1000, // Optional: timeout in ms
});

if (result.success) {
  // User authenticated
  const { address, nickname, avatar } = result.identity;
} else {
  // Authentication failed
  console.error(result.error); // Error message
  console.error(result.code); // Error code
}

Progress Tracking:

onProgress((step) => {
  switch (step.step) {
    case "opening_auth":
      console.log("Showing auth UI...");
      break;
    case "waiting_for_user":
      console.log("User is authenticating...");
      break;
    case "processing":
      console.log("Processing response...");
      break;
    case "complete":
      console.log("Auth complete!");
      break;
  }
});

Error Codes:

  • CANCELLED - User closed auth flow
  • TIMEOUT - Auth took too long (default: 5 minutes)
  • NETWORK_ERROR - Failed to load auth page
  • INVALID_CONFIG - Invalid scopes or configuration
  • AUTH_ERROR - General authentication error
signOut()

Clears session and signs out user.

await villa.signOut();
console.log("Signed out");
isAuthenticated()

Check if user has a valid session.

if (villa.isAuthenticated()) {
  console.log("User is authenticated");
}
getIdentity()

Get current user's identity (if authenticated).

const identity = villa.getIdentity();
if (identity) {
  console.log("Address:", identity.address);
  console.log("Nickname:", identity.nickname);
} else {
  console.log("Not authenticated");
}
resolveEns(name: string)

Resolve Villa ENS name to address.

const address = await villa.resolveEns("alice");
// => '0x...'
reverseEns(address: string)

Resolve address to Villa ENS name.

const name = await villa.reverseEns("0x...");
// => 'alice'
getAvatarUrl(seed: string, config?)

Generate avatar URL for a seed.

const url = villa.getAvatarUrl(address, {
  style: 'avataaars',
  seed: address
})

// Use in <img> tag
<img src={url} alt="Avatar" />
getNetwork()

Get configured network.

const network = villa.getNetwork();
// => 'base' | 'base-sepolia'
getApiUrl()

Get API endpoint URL.

const url = villa.getApiUrl();
// => 'https://api.villa.cash'

Types

Identity

User identity returned on successful authentication.

interface Identity {
  /** Ethereum address derived from passkey */
  address: `0x${string}`;

  /** User's chosen nickname */
  nickname: string;

  /** Avatar configuration */
  avatar: AvatarConfig;
}

AvatarConfig

Avatar configuration for deterministic generation.

interface AvatarConfig {
  /** DiceBear style: 'adventurer' | 'avataaars' | 'bottts' | 'thumbs' */
  style: string;

  /** Seed for generation (address, nickname, etc) */
  seed: string;

  /** Optional gender preference */
  gender?: "male" | "female" | "other";
}

SignInResult

Result from authentication.

type SignInResult =
  | {
      success: true;
      identity: Identity;
    }
  | {
      success: false;
      error: string;
      code: SignInErrorCode;
    };

type SignInErrorCode =
  | "CANCELLED"
  | "TIMEOUT"
  | "NETWORK_ERROR"
  | "INVALID_CONFIG"
  | "AUTH_ERROR";

VillaConfig

SDK configuration.

interface VillaConfig {
  /** Your application ID */
  appId: string;

  /** Network: 'base' (production) or 'base-sepolia' (testnet) */
  network?: "base" | "base-sepolia";

  /** Override API URL */
  apiUrl?: string;
}

Advanced: VillaBridge

For fine-grained control over the auth flow, use VillaBridge directly.

Basic Example

import { VillaBridge } from "@rockfridrich/villa-sdk";

const bridge = new VillaBridge({
  appId: "your-app",
  network: "base",
  timeout: 5 * 60 * 1000,
  debug: false,
});

// Subscribe to events
bridge.on("ready", () => {
  console.log("Auth UI ready");
});

bridge.on("success", (identity) => {
  console.log("Authenticated:", identity.nickname);
});

bridge.on("cancel", () => {
  console.log("User cancelled");
});

bridge.on("error", (error, code) => {
  console.error("Auth error:", error, code);
});

// Open auth flow
await bridge.open(["profile"]);

// Later...
bridge.close();

Event Handling

ready

Fired when iframe is loaded and ready.

bridge.on("ready", () => {
  // UI is ready, user can authenticate
});

success

Fired when user successfully authenticates.

bridge.on("success", (identity) => {
  const { address, nickname, avatar } = identity;
  // Save identity, redirect, etc.
});

cancel

Fired when user closes auth flow.

bridge.on("cancel", () => {
  // User cancelled authentication
  // Bridge is automatically closed
});

error

Fired when authentication fails.

bridge.on("error", (error, code) => {
  // error: error message (string)
  // code: error code (VillaErrorCode)

  if (code === "TIMEOUT") {
    console.error("Auth took too long");
  } else if (code === "NETWORK_ERROR") {
    console.error("Network error:", error);
  }
});

consent_granted

Fired when user grants consent to access their data.

bridge.on("consent_granted", (appId, scopes) => {
  console.log(`Consent granted for ${appId} with scopes:`, scopes);
});

consent_denied

Fired when user denies consent.

bridge.on("consent_denied", (appId) => {
  console.log(`Consent denied for ${appId}`);
});

VillaBridge Configuration

interface BridgeConfig {
  /** Your application ID (required) */
  appId: string;

  /** Override Villa auth origin (defaults to production) */
  origin?: string;

  /** Network: 'base' or 'base-sepolia' (default: 'base') */
  network?: "base" | "base-sepolia";

  /** Timeout in milliseconds (default: 5 minutes) */
  timeout?: number;

  /** Enable debug logging (default: false) */
  debug?: boolean;

  /** Prefer popup over iframe (default: false) */
  preferPopup?: boolean;

  /** Timeout to detect iframe blocking in ms (default: 3 seconds) */
  iframeDetectionTimeout?: number;
}

VillaBridge Methods

open(scopes?)

Open auth iframe.

await bridge.open(["profile", "wallet"]);

close()

Close auth iframe and clean up.

bridge.close();

on(event, callback)

Subscribe to event.

const unsubscribe = bridge.on("success", (identity) => {
  console.log("Success!");
});

// Later...
unsubscribe();

off(event, callback)

Unsubscribe from event.

const handler = (identity) => {
  /* ... */
};
bridge.on("success", handler);
// Later...
bridge.off("success", handler);

removeAllListeners(event?)

Remove all listeners for an event (or all events).

bridge.removeAllListeners("success");
bridge.removeAllListeners(); // Remove all

getState()

Get current bridge state.

const state = bridge.getState();
// => 'idle' | 'opening' | 'ready' | 'authenticating' | 'closing' | 'closed'

isOpen()

Check if bridge is open.

if (bridge.isOpen()) {
  // Bridge is ready or authenticating
}

postMessage(message)

Send custom message to iframe (advanced).

bridge.postMessage({ type: "CUSTOM_MESSAGE", payload: {} });

Authentication Flow

How It Works

  1. App calls villa.signIn()

    • SDK creates fullscreen iframe with Villa auth UI
  2. User creates or uses passkey

    • WebAuthn handles passkey generation/authentication
    • Passkey never leaves user's device
  3. User chooses nickname and avatar

    • Customizes their Villa identity
  4. Villa signs message with passkey

    • Creates signature proving ownership of passkey
  5. Address derived from signature

    • Deterministic Ethereum address calculated
    • No wallet, no seed phrase
  6. Identity sent back via postMessage

    • Secure channel between iframe and parent
    • Origin validation on both sides
  7. Session stored in localStorage

    • 7-day expiry
    • Automatically cleared on signOut()

Popup vs Iframe

Iframe (default)

  • Opens in fullscreen modal
  • No popup blockers
  • Seamless UX
  • Recommended for all apps

Automatic Popup Fallback

  • If iframe is blocked, SDK automatically falls back to popup
  • Detection timeout: 3 seconds (configurable)
  • Popup automatically closes after auth completes

Force Popup Mode

const bridge = new VillaBridge({
  appId: "your-app",
  preferPopup: true, // Skip iframe, go straight to popup
});

Handle Popup Blocked

bridge.on("error", (error, code) => {
  if (code === "NETWORK_ERROR" && error.includes("Popup blocked")) {
    alert("Please allow popups for this site to authenticate");
  }
});

Error Handling

Common Errors & Solutions

| Error | Cause | Solution | | -------------------- | -------------------- | --------------------------------------- | | appId is required | Missing config.appId | Pass appId to constructor | | Connection timeout | User taking too long | Increase timeout option | | NETWORK_ERROR | Failed to load auth | Check network, try again | | CANCELLED | User closed auth | Handled by app (expected) | | INVALID_CONFIG | Bad scopes | Use valid scopes: ['profile', 'wallet'] |

Development Tips

Enable debug logging:

const bridge = new VillaBridge({
  appId: "your-app",
  debug: true, // Logs all messages
});

Check iframe in DevTools:

  • Open Chrome DevTools
  • Go to Elements tab
  • Look for #villa-auth-iframe
  • Inspect postMessage in Console

Passkey issues:

  • Ensure you're on HTTPS (or localhost for dev)
  • Use pnpm dev:https for local passkey testing
  • iOS requires iOS 16+
  • Android requires Android 9+ (with compatible authenticator)

Utilities

resolveEns(name: string)

import { resolveEns } from "@rockfridrich/villa-sdk";

const address = await resolveEns("alice");

reverseEns(address: string)

import { reverseEns } from "@rockfridrich/villa-sdk";

const name = await reverseEns("0x...");

getAvatarUrl(seed: string, config?)

import { getAvatarUrl } from "@rockfridrich/villa-sdk";

const url = getAvatarUrl("0x...", {
  style: "avataaars",
  seed: "0x...",
});

getContracts(network?)

Get contract addresses for a network.

import { getContracts } from "@rockfridrich/villa-sdk";

const contracts = getContracts("base");
// => {
//   nicknameResolver: '0x...',
//   recoverySigner: '0x...'
// }

Networks

| Network | Chain ID | Use Case | | ---------------- | -------- | ---------- | | Base | 8453 | Production | | Base Sepolia | 84532 | Testing |

React Integration

For React apps, use @rockfridrich/villa-sdk-react:

npm install @rockfridrich/villa-sdk-react
import { VillaProvider, VillaAuth, useIdentity } from '@rockfridrich/villa-sdk-react'

function App() {
  return (
    <VillaProvider config={{ appId: 'your-app' }}>
      <LoginPage />
    </VillaProvider>
  )
}

function LoginPage() {
  const identity = useIdentity()

  if (!identity) {
    return <VillaAuth onComplete={(result) => {}} />
  }

  return <h1>Welcome, @{identity.nickname}!</h1>
}

Session Management

Automatic Session Persistence

Sessions are automatically saved to localStorage:

// Session saved after signIn()
villa.isAuthenticated(); // true
villa.getIdentity(); // returns identity

// Even after page reload
// Session restored from localStorage

Session Expiry

Sessions expire after 7 days:

// Session is automatically cleared after 7 days
// getIdentity() returns null
// isAuthenticated() returns false

Manual Sign Out

await villa.signOut();
// localStorage cleared
// currentSession = null

Type Safety

All exports are fully typed with TypeScript:

import type {
  Identity,
  AvatarConfig,
  SignInResult,
  VillaConfig,
  SignInErrorCode,
  VillaSession,
} from "@rockfridrich/villa-sdk";

AI Integration

This package includes CLAUDE.txt and llms.txt for AI coding assistants.

One-prompt integration: Just tell your AI assistant:

"Add Villa authentication to my app"

Works with Claude Code, Cursor, Windsurf, and Lovable.

Troubleshooting

Blank Auth Page

Symptoms: Auth iframe shows blank page

Solution:

# Clear build cache and restart
pnpm dev:clean

Passkeys Not Working

Symptoms: Passkey creation fails or no biometric option

Solution:

# Use HTTPS (local dev with mkcert)
pnpm dev:https

# Or test with Base Sepolia testnet
const villa = new Villa({ appId: 'test', network: 'base-sepolia' })

Origin Validation Error

Symptoms: "Origin not in allowlist" error

Solution:

  • Don't provide custom origin in production
  • Use network defaults (base or base-sepolia)
  • Contact support for custom origin allowlisting

Session Not Persisting

Symptoms: getIdentity() returns null after reload

Solution:

// Check localStorage
console.log(localStorage.getItem("villa:session"));

// Sessions require:
// 1. localStorage available
// 2. Valid session data
// 3. Not expired (7 days)

Links

License

MIT

Authentication Utilities

The SDK includes low-level utilities for building custom authentication flows:

  • WebAuthn error handling - Parse and display user-friendly error messages
  • Browser capability detection - Detect available passkey features
  • Passkey manager detection - Identify 1Password, iCloud, Google, etc.
  • Porto configuration helpers - Validate and configure Porto SDK

See AUTH-UTILITIES.md for detailed documentation and examples.

import {
  detectBrowserCapabilities,
  parseWebAuthnError,
  isPasskeySupported,
  getPasskeyManagerName,
} from "@rockfridrich/villa-sdk";

// Check if passkeys are supported
if (!isPasskeySupported()) {
  console.error("Passkeys not supported");
}

// Detect capabilities
const caps = await detectBrowserCapabilities();
console.log("Platform auth available:", caps.platformAuthenticatorAvailable);
console.log("Passkey managers:", caps.passkeyManagers);

// Handle WebAuthn errors gracefully
try {
  await navigator.credentials.create(options);
} catch (error) {
  const webAuthnError = parseWebAuthnError(error);
  if (webAuthnError.shouldDisplay) {
    showError(webAuthnError.userMessage);
  }
}