@rockfridrich/villa-sdk
v0.1.3
Published
Villa Identity SDK - Privacy-first passkey authentication for Base network
Maintainers
Readme
@rockfridrich/villa-sdk
Privacy-first passkey authentication for Base network. No wallets. No passwords. Just Face ID.
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 minifiedQuick Start
npm install @rockfridrich/villa-sdk viem zodSimple 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 neededVilla Client (Advanced)
For apps needing custom configuration:
Constructor
const client = new Villa(config);Parameters:
config.appId(string, optional) - Application identifier for analyticsconfig.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 flowTIMEOUT- Auth took too long (default: 5 minutes)NETWORK_ERROR- Failed to load auth pageINVALID_CONFIG- Invalid scopes or configurationAUTH_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 allgetState()
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
App calls
villa.signIn()- SDK creates fullscreen iframe with Villa auth UI
User creates or uses passkey
- WebAuthn handles passkey generation/authentication
- Passkey never leaves user's device
User chooses nickname and avatar
- Customizes their Villa identity
Villa signs message with passkey
- Creates signature proving ownership of passkey
Address derived from signature
- Deterministic Ethereum address calculated
- No wallet, no seed phrase
Identity sent back via postMessage
- Secure channel between iframe and parent
- Origin validation on both sides
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:httpsfor 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-reactimport { 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 localStorageSession Expiry
Sessions expire after 7 days:
// Session is automatically cleared after 7 days
// getIdentity() returns null
// isAuthenticated() returns falseManual Sign Out
await villa.signOut();
// localStorage cleared
// currentSession = nullType 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:cleanPasskeys 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
originin 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);
}
}