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

venm-auth

v1.6.2

Published

React authentication SDK for Venm — OAuth, session management, React hooks & components

Readme

venm-auth

Full-stack authentication SDK for Venm — integrate Google & Facebook OAuth, passwordless Phone & SMS/WhatsApp OTP login, and secure Express API route protection middleware into any React & Node.js application with minimal code.

Key Features

  • 🔐 Multi-Provider OAuth: Turnkey Google (Web & Native Capacitor One-Tap) and Facebook OAuth login flows.
  • 📱 Passwordless Phone & SMS/WhatsApp OTP: Comprehensive server routes for one-time code verification, mobile number login, and 2FA workflows.
  • 🛡️ Express Route Protection & Security Middleware: Turnkey JWT verification (createAuthMiddleware), route guards (requireAuth), CSRF state validation, and API rate limiting.
  • 🗄️ Universal Database Adapters: Built-in MongoDB adapter with automatic id / _id document normalization and refresh token TTL cleanup.
  • ⚡ Real-Time Event Hooks: Global event emitters for login lifecycle notifications and automated referral commission attribution.

Table of Contents

What's New

🪪 Universal ID & ObjectId Normalization (v1.4.1)

The MongoDB adapter (createMongoDBAdapter) now provides universal document ID normalization, eliminating collision and lookup failures between MongoDB ObjectIds (_id) and application UUIDs (id):

  • All database operations (findUserById, updateUser, createSession, findSessionByToken) safely check both id and _id filters.
  • Document serializers (toUser, toSession) consistently guarantee a top-level .id string across all returned entities.
  • Added native collection safety wrappers (getCol) so custom adapters and test mocks can interact with extended collections without runtime getNativeCollection undefined errors.

🎨 Tailwind CSS Native Components (v1.4.1)

  • All venm-auth UI components (<GoogleButton>, <FacebookButton>, <VenmAuth>, etc.) are now natively built with Tailwind CSS.
  • Inline styles have been entirely removed, making every component 100% customizable via the className prop using standard Tailwind utility classes.
  • (Note: Consumers must add venm-auth to their tailwind.config content array for classes to compile).

🛡️ Mongoose 7/8+ & Modern Driver Compatibility (v1.4.1)

  • Fully upgraded query operations across Express backends to use modern syntax (returnDocument: 'after'), eliminating Mongoose deprecation warnings.
  • Expanded ServerSession and CreateSessionData types to natively capture client security metadata: userAgent, ipAddress, and deviceInfo.
  • Strengthened TypeScript declarations for 2FA/TOTP (otplib) and optional user status properties (roles, status, is2faEnabled).

📱 Enhanced Native Capacitor One Tap

The Google One Tap integration for Capacitor has been significantly improved:

  • Simplified Client IDs: You no longer need to provide separate Android and iOS Client IDs in your config! The SDK and Server now universally use your single Web Application Client ID for initialization and token audience verification. Native platform validation is handled securely by Google Play Services (via SHA-1) and iOS (via URL schemes).
  • Flexible Native Flows: The <GoogleButton> component now accepts a nativeFlow prop to customize the native Google Sign-in experience:
    • "autoOrOneTap" (default): Attempts silent auto sign-in, falling back to the One Tap bottom sheet.
    • "oneTap": Forces the One Tap bottom sheet to appear.
    • "nativeButton": Triggers the traditional full-screen Google Sign-In prompt.
    • All native flows include an automatic, safe fallback to the browser-based OAuth popup if the Capacitor plugin is unavailable or fails!

🛡️ React 18 StrictMode Safety

Session initialization now uses a cancellation flag to safely handle React 18 StrictMode's double-mount behavior in development. Previously, two parallel initialize() calls could race — the first would rotate tokens on the server, the second would fail with SESSION_NOT_FOUND, clear localStorage, and log the user out. The cancellation flag ensures stale callbacks from unmounted effects are ignored.

🔁 Concurrent Refresh Guard

SessionService.refreshSession() now returns the same in-flight promise for all concurrent callers. Whether triggered by StrictMode double-mount or rapid manual refreshes, duplicate calls share a single promise instead of racing and corrupting token rotation.

⏰ Session Expiry Callback

When auto-refresh fails (e.g., expired refresh token), the SDK now fires an onRefreshFailed callback that:

  • Clears the session from localStorage
  • Dispatches UNAUTHENTICATED with a SESSION_EXPIRED error code
  • Provides a clean path for redirecting users back to the login screen

⚡ Extended Token Refresh Margin

The auto-refresh margin has been increased from 60 seconds → 120 seconds before token expiry, giving more headroom for network latency and server-side token rotation.

🗄️ Refresh Token Expiry in Database

Added refreshExpiresAt field to the ServerSession and CreateSessionData types. The MongoDB adapter now stores this field, enabling native TTL index cleanup of expired refresh tokens. The server also supports passing accessTokenExpiresIn and refreshTokenExpiresIn through the session config to override default token lifetimes.

🐛 Bug Fixes

  • Memory adapter token rotation: Fixed the demo server's memory adapter to properly clean up old token keys when tokens are rotated during refresh. Previously, stale keys would accumulate and shadow updated sessions.
  • Cleaner dev experience: The demo app now includes an auth event log, live access token countdown, manual refresh button, provider/layout toggles, and improved profile badges.

Installation

👋 Quick start: bun add venm-auth (or pnpm, npm, yarn) — React 18+, Node.js 18+ required.

Prerequisites

| Requirement | Version | |-------------|---------| | Node.js | >= 18 | | React | ^18.2.0 (peer dependency) | | React DOM | ^18.2.0 (peer dependency) |

Client-side

# bun (recommended)
bun add venm-auth

# pnpm
pnpm add venm-auth

# npm
npm install venm-auth

# yarn
yarn add venm-auth

Server-side (Express)

The server package is bundled within venm-auth — no additional install needed.

import { createVenmAuth, createMongoDBAdapter } from "venm-auth/server";

Environment Variables

Create a .env file in your server project:

# Google OAuth
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret

# Facebook OAuth
FACEBOOK_APP_ID=your-app-id
FACEBOOK_APP_SECRET=your-app-secret

# JWT signing key (min 32 characters)
JWT_SECRET=your-256-bit-secret-key-change-me

# MongoDB (if using the built-in adapter)
MONGODB_URI=mongodb://localhost:27017/myapp

💡 Tip: The JWT secret must be at least 32 characters long. You can generate one with openssl rand -base64 32.

Client Implementation

import { VenmProvider, VenmAuth, Authenticated, Unauthenticated, Loading, useAuth } from "venm-auth";
import type { SDKConfig, ProviderType, Layout } from "venm-auth";

// ── Configuration ───────────────────────────────────────────────────

const config: SDKConfig = {
  // Base URL of your Express auth server (required)
  //   Development: "http://localhost:3000/api/auth"
  //   Production:  "/api/auth"
  apiUrl: "http://localhost:3000/api/auth",

  // SDK environment
  //   "development" — enables verbose logging
  //   "production"  — (default) minimal logging
  environment: "development",

  // Automatically refresh access tokens before they expire (default: true)
  autoRefresh: true,

  // Persist session to localStorage (default: true)
  persistSession: true,

  // Storage mechanism for session persistence (default: "localStorage")
  //   "localStorage"  — survives tab close
  //   "sessionStorage" — cleared on tab close
  storage: "localStorage",

  // HTTP request timeout in milliseconds (default: 10000)
  timeout: 10000,

  // Optional override for OAuth callback URI (defaults on server to "/api/auth/google/callback" or "/api/auth/facebook/callback")
  redirectUri: "http://localhost:3000/api/auth/google/callback",

  // OAuth provider credentials (required — at least one provider)
  oauth: {
    google: {
      clientId: "your-google-client-id.apps.googleusercontent.com",
    },
    facebook: {
      appId: "your-facebook-app-id",
    },
  },
};

// ── App Root ─────────────────────────────────────────────────────────

export default function App() {
  return (
    <VenmProvider
      config={config}
      // Called whenever auth state changes (login, logout, token refresh)
      onAuthStateChange={(state) => {
        console.log("[auth] State:", state);
      }}
    >
      <AppContent />
    </VenmProvider>
  );
}

// ── App Content ──────────────────────────────────────────────────────

function AppContent() {
  const { logout } = useAuth();

  return (
    <div>
      {/* Shows a loading spinner while the session initializes */}
      <Loading>
        <p>Loading session...</p>
      </Loading>

      {/* Shown when the user is NOT authenticated */}
      <Unauthenticated>
        {/* Renders OAuth provider buttons */}
        {/* layouts: "vertical" | "horizontal" | "card" | "minimal" */}
        <VenmAuth
          providers={["google", "facebook"] as ProviderType[]}
          layout="card"         // (default: "vertical")
          showDivider={false}   // Show "or" divider between buttons
        />
      </Unauthenticated>

      {/* Shown when the user IS authenticated */}
      <Authenticated>
        <h1>Welcome!</h1>
        <button onClick={() => logout()}>Sign Out</button>
      </Authenticated>
    </div>
  );
}

🎨 Styling (Tailwind CSS)

All venm-auth components are built natively with Tailwind CSS and accept a className prop for seamless customization. To ensure the default styles compile correctly in your application, you must include the package in your tailwind.config's content array:

// tailwind.config.ts
export default {
  content: [
    "./src/**/*.{ts,tsx}",
    "./node_modules/venm-auth/**/*.{ts,tsx,js,jsx}", // <-- Add this line
    "../../node_modules/venm-auth/**/*.{ts,tsx,js,jsx}" // (If using monorepos/workspaces)
  ],
  // ...
};

You can then override or extend styles simply by passing classes:

<GoogleButton className="!bg-black !text-white hover:opacity-80 rounded-full" />

Server Implementation & Production Guide

The server package (venm-auth/server) provides modular routers, database adapters, authentication middleware, and real-time event hooks to power your full-stack backend.

1. Core Router Initialization

Mount the primary authentication router to handle Google/Facebook OAuth redirect flows, callback token exchanges, session management, and user creation:

import express from "express";
import { createVenmAuth, createMongoDBAdapter } from "venm-auth/server";
import type { VenmAuthConfig } from "venm-auth/server";

const app = express();
app.use(express.json());

// ── Database Adapter ────────────────────────────────────────────────
const database = createMongoDBAdapter({
  uri: process.env.MONGODB_URI!,
  databaseName: "myapp_db",
});

// ── Configuration ───────────────────────────────────────────────────
const authConfig: VenmAuthConfig = {
  google: {
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  },
  facebook: {
    appId: process.env.FACEBOOK_APP_ID!,
    appSecret: process.env.FACEBOOK_APP_SECRET!,
  },
  jwtSecret: process.env.JWT_SECRET!, // Minimum 32 characters
  database,
  session: {
    accessTokenExpiresIn: "15m",
    refreshTokenExpiresIn: "30d",
  },
  prefix: "/api/auth",
  allowedOrigins: ["http://localhost:3000", "https://yourdomain.com"],
  // Natively handle multi-level referrals and attribution
  referral: {
    enabled: true,
    maxLevels: 3,
    requireAdminApproval: true,
  },
  events: {
    onReferralApplied: async ({ referrer, referee, level }) => {
      console.log(`Referral applied! ${referee} joined under ${referrer} (level ${level})`);
    },
    onCommissionEarned: async ({ referrerId, refereeId, amount, level }) => {
      console.log(`User ${referrerId} earned ${amount} from ${refereeId} (level ${level})`);
      // e.g. await creditWallet(referrerId, amount, 'REFERRAL_COMMISSION');
    },
    onOtpRequested: async (phone, code) => {
      // Integrate with Twilio, AWS SNS, or WhatsApp Business API
      console.log(`Sending OTP ${code} to ${phone}`);
    }
  }
};

// ── Mount Auth Router ───────────────────────────────────────────────
app.use("/api/auth", createVenmAuth(authConfig));

2. Authentication & Route Protection Middleware

venm-auth/server exports ready-to-use Express middleware to protect private API endpoints, enforce role-based access control (RBAC), and defend against abuse:

| Middleware / Helper | Type | Description | |---------------------|------|-------------| | createAuthMiddleware({ jwtSecret, database }) | Factory | Resolves Bearer JWT from Authorization header, verifies signature, loads user from database adapter, and populates req.user and req.session. | | requireAuth | Middleware | Enforces authentication; immediately rejects unauthenticated requests with HTTP 401 Unauthorized. | | stateCookieMiddleware | Middleware | Sets secure, httpOnly, same-site CSRF state cookies during OAuth initiation. | | validateState | Middleware | Validates OAuth callback parameters against stored CSRF state cookies to prevent login forgery. | | oauthRateLimiter | Middleware | Rate limits OAuth token exchange endpoints (default: 10 requests / minute / IP). | | resultRateLimiter | Middleware | Rate limits COOP polling endpoints (default: 30 requests / minute / session). | | errorHandler | Middleware | Catches and standardizes all VenmAuthError, CSRF, and rate limit errors into a consistent JSON response. |

Example: Protecting Private Routes & Enforcing Roles

import { createAuthMiddleware, requireAuth } from "venm-auth/server";
import type { Request, Response, NextFunction } from "express";

const auth = createAuthMiddleware({
  jwtSecret: process.env.JWT_SECRET!,
  database,
});

// Admin role check helper
function requireAdmin(req: Request, res: Response, next: NextFunction) {
  if (!req.user || !req.user.roles?.includes("ADMIN")) {
    return res.status(403).json({ error: { message: "Admin access required" } });
  }
  next();
}

// Secure admin endpoint
app.get("/api/admin/users", auth, requireAuth, requireAdmin, async (req, res) => {
  const users = await database.findUsers({});
  res.json({ users });
});

3. Real-Time Event Hooks & Referral Attribution

venm-auth/server exports a global event emitter (venmAuthEvents) that allows your application services to react asynchronously to authentication lifecycle and referral program events without coupling core auth logic:

| Event Name | Payload | Description | |------------|---------|-------------| | user:created | (user: User) | Emitted when a new user registers | | user:deleted | ({ id, _id, email, phone }) | Emitted when a user account is deleted | | user:login | (user: User, device?: Device) | Emitted when a user successfully logs in | | user:logout | (user: User) | Emitted when a user logs out | | referral:applied | (referral: Referral) | Emitted when a referral code is applied to a user | | referral:commission | ({ referrerId, refereeId, level, amount }) | Emitted when a referrer earns commission | | referral:trigger_distribution | ({ refereeId, baseAmount }) | Triggers distribution of multi-level commissions |

import { venmAuthEvents } from "venm-auth/server";

// Listen for successful user login/registration
venmAuthEvents.on("user:login", (user, device) => {
  console.log(`User ${user.email} logged in on device ${device?.userAgent}`);
});

// Listen for referral commission attribution (e.g. credit user wallet natively)
venmAuthEvents.on("referral:commission", async ({ referrerId, refereeId, amount, level }) => {
  console.log(`Referral reward: User ${referrerId} earned $${amount} from level ${level} referral!`);
  // Example: creditWallet(referrerId, amount);
});

4. Phone & SMS OTP Authentication

For mobile or passwordless SMS/WhatsApp workflows, mount the supplementary phone authentication routes:

import { createPhoneAuthRoutes } from "venm-auth/server";

app.use("/api/auth/phone", createPhoneAuthRoutes({
  database,
  jwtSecret: process.env.JWT_SECRET!,
  sendOtp: async (phone, otp) => {
    // Integrate with Twilio, AWS SNS, or WhatsApp Business API
    console.log(`Sending OTP ${otp} to ${phone}`);
  },
}));

Client Workflow

When createPhoneAuthRoutes({ database, jwtSecret, sendOtp }) is mounted to /api/auth/phone, your client application can authenticate users via SMS or WhatsApp without passwords:

  1. Request OTP: Client sends POST /api/auth/phone/request-otp with { "phone": "+1234567890" }. The server stores a hashed OTP and calls your sendOtp callback.
  2. Verify OTP: Client sends POST /api/auth/phone/verify-otp with { "phone": "+1234567890", "code": "123456" }. Upon success, the server returns JWT access/refresh tokens and the authenticated user object.

OAuth Flow

  1. User clicks a provider button (Google/Facebook)
  2. A popup opens to your Express server's OAuth authorize endpoint (GET /google?state=...&code_challenge=...&auth_session_id=...)
  3. The server sets a signed CSRF state cookie, stores a state-to-session mapping, and redirects to the provider's consent screen
  4. The user authenticates with the chosen provider
  5. The provider redirects back to your server's callback endpoint (GET /google/callback)
  6. The server validates the state cookie against the returned state parameter, then stores the authorization code server-side keyed by the auth_session_id (see COOP Handling below)
  7. The main page polls GET /result/:authSessionId every 2 seconds until the code is available
  8. The popup closes; the SDK exchanges the code for tokens via POST /google
  9. The server exchanges the code with the provider, creates/updates the user in the database, generates JWT tokens, and stores the session
  10. The user and session are stored in React state and localStorage
  11. The onAuthStateChange callback fires with the new auth state

Why server-side storage? Some OAuth providers (notably Google) set Cross-Origin-Opener-Policy: same-origin on their consent pages, which severs the window.opener reference in the popup — making postMessage silently fail. Instead, the callback stores the OAuth result on the server, and the main page polls GET /result/:authSessionId to retrieve it. See the COOP section for details.

Ecosystem Integration (Admin & Consumer Monorepos)

venm-auth natively supports multi-app monorepos where you have separate Consumer and Admin applications that share the same backend but require isolated authentication states and distinct route protection.

1. Backend Setup (Dual Routers)

Mount two isolated routers on your Express server. One handles OAuth and Consumer logic, the other handles Admin Email/Password authentication.

import { createVenmAuth, createAdminUsersRoutes } from "venm-auth/server";

// 1. Consumer Router (OAuth, Phone, Public Users)
app.use("/api/v1/auth", createVenmAuth({
  google: { clientId: "...", clientSecret: "..." },
  jwtSecret: process.env.JWT_SECRET!,
  database,
  prefix: "/api/v1/auth",
}));

// 2. Admin Router (Email/Password, RBAC)
app.use("/api/v1/admin/auth", createAdminUsersRoutes({
  database,
  jwtSecret: process.env.JWT_SECRET!,
  // Optional: override JWT lifetimes for admins
  session: { accessTokenExpiresIn: "1h", refreshTokenExpiresIn: "1d" },
}));

2. Main Consumer App Setup

In your main React app, initialize the provider pointing to your Consumer endpoint and use the standard useAuth hook.

// main-app/src/App.tsx
import { VenmProvider, VenmAuth, Authenticated, Unauthenticated } from "venm-auth";

export default function App() {
  return (
    <VenmProvider config={{ apiUrl: "/api/v1/auth", environment: "production" }}>
      <Unauthenticated>
        <VenmAuth providers={["google", "facebook"]} />
      </Unauthenticated>
      <Authenticated>
         <Dashboard />
      </Authenticated>
    </VenmProvider>
  );
}

3. Admin App Setup

In your separate admin React app, initialize the provider pointing to your Admin endpoint. Exclusively use the useAdminAuth hook and Admin components (<AdminLoginForm>, <AdminRegisterForm>). Running your Admin app on a different subdomain (e.g., admin.domain.com) inherently prevents localStorage token conflicts.

// admin-app/src/App.tsx
import { VenmProvider, AdminLoginForm, useAdminAuth } from "venm-auth";

function AdminRoot() {
  return (
    <VenmProvider config={{ apiUrl: "/api/v1/admin/auth", environment: "production" }}>
      <AdminRouter />
    </VenmProvider>
  );
}

function AdminLogin() {
  const { session } = useAdminAuth();

  if (session) return <Navigate to="/dashboard" />;

  return (
    <div className="max-w-sm mx-auto mt-20">
      <h1 className="text-2xl font-bold mb-4">Admin Portal</h1>
      <AdminLoginForm onSuccess={() => console.log("Admin Logged In!")} />
    </div>
  );
}

Integration Guides

venm-auth is designed to drop seamlessly into a variety of architectures and frameworks. Below are specific guides for integrating with common tools.

1. Capacitor & Native iOS/Android (Google One Tap/Native)

For hybrid mobile applications built with Ionic Capacitor, venm-auth handles the complexity of Native Google Sign-In and Apple Sign-In.

  • One Client ID: You only need your Web Application Client ID. Do NOT configure separate iOS or Android Client IDs in the VenmAuthConfig. Native client validation is handled securely by Google Play Services and iOS URL schemes.
  • Native Flow Selection: The <GoogleButton> accepts a nativeFlow prop:
    • "autoOrOneTap": Attempts silent auto sign-in, falling back to One Tap bottom sheet.
    • "oneTap": Forces the One Tap bottom sheet.
    • "nativeButton": Full-screen native Google Sign-In prompt.
  • Fallback: If the native Capacitor plugin fails or the app is running in a web browser, it safely falls back to the standard web OAuth popup.

2. Phone & WhatsApp OTP Integration (Twilio/Infobip)

Integrate passwordless SMS or WhatsApp login by mounting the Phone Auth routes on your Express server and providing a sendOtp callback.

import { createPhoneAuthRoutes } from "venm-auth/server";
import twilio from "twilio";

const client = twilio(process.env.TWILIO_SID, process.env.TWILIO_AUTH_TOKEN);

app.use("/api/auth/phone", createPhoneAuthRoutes({
  database,
  jwtSecret: process.env.JWT_SECRET!,
  sendOtp: async (phone, otp) => {
    // Send SMS via Twilio
    await client.messages.create({
      body: `Your verification code is: ${otp}`,
      from: process.env.TWILIO_PHONE_NUMBER,
      to: phone
    });
  },
}));

3. Frontend Frameworks: Next.js SSR vs Vite React SPA

Vite React SPA

For standard Single Page Applications, simply wrap your root component in <VenmProvider>. Authentication state is managed client-side in React State and localStorage.

Next.js App Router (SSR)

If using Next.js App Router, venm-auth components should be placed inside Client Components (files marked with "use client"). Server-side route protection should be handled by Next.js middleware by verifying the JWT token stored in cookies (if you configure your Express backend to set httpOnly cookies) or by passing the Bearer token manually to Next.js API routes.

4. Admin vs Consumer Monorepo Architectures

venm-auth natively supports multi-app monorepos with separate Consumer and Admin portals.

  • Dual Routers: Mount two isolated routers on your Express server (/api/v1/auth for Consumers, /api/v1/admin/auth for Admins).
  • Isolated State: Run your Admin app on a separate subdomain (e.g., admin.domain.com). This completely isolates localStorage tokens.
  • Admin Hooks: Use the dedicated useAdminAuth() hook and <AdminLoginForm> component in your Admin app.

5. Referral Program & Gamification

venm-auth includes a built-in multi-level referral attribution engine. By enabling the referral config on the server, you can listen to real-time events to power gamification.

const authConfig = {
  // ...
  referral: {
    enabled: true,
    maxLevels: 3,
    requireAdminApproval: true,
  },
  events: {
    onCommissionEarned: async ({ referrerId, refereeId, amount, level }) => {
      // Reward the referrer in your core gamification/economy service!
      await walletService.creditUser(referrerId, amount, { category: 'referral_bonus' });
    }
  }
};

Server API Endpoints

| Method | Path | Description | |--------|------|-------------| | GET | /google | Initiate Google OAuth redirect (sets CSRF cookie) | | GET | /google/callback | Google OAuth callback (validates CSRF cookie, stores result server-side) | | POST | /google | Exchange Google auth code for JWT tokens | | POST | /google/onetap | Verify Google One-Tap / Capacitor ID token and generate JWT tokens | | GET | /facebook | Initiate Facebook OAuth redirect (sets CSRF cookie) | | GET | /facebook/callback | Facebook OAuth callback (validates CSRF cookie, stores result server-side) | | POST | /facebook | Exchange Facebook auth code for JWT tokens | | POST | /phone/request-otp | Send a one-time verification code (OTP) via SMS or WhatsApp | | POST | /phone/verify-otp | Verify OTP and authenticate/create user profile (accepts optional referralCode body field) | | POST | /2fa/verify | Verify TOTP or WhatsApp 2FA code during login | | POST | /register-device | Register a new device session (accepts optional referralCode body field) | | POST | /referral/apply | Manually apply a referral code to the authenticated user | | GET | /result/:authSessionId | Poll for OAuth auth result (bypasses COOP-broken postMessage) | | GET | /session | Verify and return current session (requires Bearer token) | | POST | /refresh | Refresh access token using refresh token | | GET | /user | Return current user (requires Bearer token) | | POST | /logout | Destroy current session | | POST | /logout/all | Destroy all sessions for the user | | GET | /health | Health check |

Components

VenmProvider

The root component. Must wrap all authentication-using code.

| Prop | Type | Default | Description | |------|------|---------|-------------| | config | SDKConfig | — | SDK configuration (apiUrl, oauth, environment, etc.) | | onAuthStateChange | (state: AuthState) => void | — | Called when auth state changes |

VenmAuth

Renders OAuth provider buttons in a configurable layout.

| Prop | Type | Default | Description | |------|------|---------|-------------| | providers | ProviderType[] | ["google", "facebook"] | Which provider buttons to show | | layout | Layout | "vertical" | "vertical", "horizontal", "card", or "minimal" | | showDivider | boolean | false | Show "or" divider between buttons |

GoogleButton / FacebookButton

Standalone provider buttons.

| Prop | Type | Default | Description | |------|------|---------|-------------| | onClick | () => void | — | Override default login handler | | disabled | boolean | false | Disable the button | | loading | boolean | false | Show loading spinner | | children | ReactNode | — | Custom button label |

Authenticated / Unauthenticated / Loading

Conditional rendering components.

<Authenticated fallback={<LoginPage />}>
  <Dashboard />
</Authenticated>

<Unauthenticated>
  <LoginPage />
</Unauthenticated>

<Loading>
  <p>Loading...</p>
</Loading>

Hooks

| Hook | Returns | Description | |------|---------|-------------| | useAuth() | { user, session, loading, error, login, logout, refresh } | Full authentication state and methods | | useUser() | { user, loading } | Current user only | | useSession() | { accessToken, refreshToken, expiresAt, loading } | Session tokens only | | useLogin() | { login, loading, error } | Login method with loading state | | useLogout() | { logout, loading } | Logout method with loading state |

Cross-Origin-Opener-Policy (COOP) Handling

The Problem

Google's OAuth consent screen sets the Cross-Origin-Opener-Policy: same-origin HTTP header on its pages. This severs the window.opener reference in the popup, so window.opener.postMessage() silently fails — the popup closes but the main page never receives the auth code.

Facebook does not set COOP headers, so postMessage works for Facebook logins. The fallback handles both cases transparently.

The Solution: Server-Side Result Relay

The SDK uses a dual-delivery mechanism:

  1. Fast path (postMessage) — The callback HTML always attempts window.opener.postMessage(). Resolves instantly when it works (e.g., Facebook).
  2. Fallback path (polling) — In parallel, the client polls GET /result/:authSessionId every 2 seconds to retrieve the OAuth result. Ensures the flow works even when COOP severs the opener.

Whichever path resolves first wins.

Architecture

 Main Page                  Express Auth Server
    │                              │
    ├── 1. Open popup + auth_session_id ──▶  │
    │                              │
    ├── 2. Poll GET /result/:id ──▶  │
    │    (every 2s, returns        │
    │     { status: "PENDING" })   │
    │ ◀────────────────────────────┤
    │                              │
    │           ┌──────────────┐   │
    │           │   Google     │   │
    │           │   Consent    │───┼── 3. POST /google/callback ──▶ storeResult(state, code)
    │           │   Screen     │   │
    │           └──────────────┘   │
    │                              │
    ├── 4. Next poll: { code, state } ◀─┤
    │                              │
    ├── 5. POST /google (exchange) ──▶  │
    │                              │

The OauthResultStore

An in-memory store that maps OAuth state parameters to client-generated authSessionId values:

| Step | Action | Description | |------|--------|-------------| | Authorization request | setStateMapping(state, authSessionId) | Stores state → authSessionId | | OAuth callback | storeResult(state, code) | Looks up authSessionId from state, stores result | | Client polling | getResult(authSessionId) | Returns result and deletes it (one-time retrieval) | | Error callback | storeError(state, errorMessage) | Stores error so the client receives it promptly |

  • TTL: Results expire after 5 minutes (cleanup runs every 60 seconds)
  • One-time retrieval: Once read, the result is immediately deleted
  • No console noise: Returns 200 { status: "PENDING" } while waiting (not 404)

Rate Limiting

Dedicated resultRateLimiter — 30 requests per minute per authSessionId (compared to 10/min for other OAuth endpoints), matching the 2-second polling interval.

Security

  • PKCE (Proof Key for Code Exchange) for Google OAuth — protects against authorization code interception
  • Cookie-based CSRF state — state parameter stored in a signed, httpOnly, short-lived cookie; validated on callback
  • Server-side result storage — auth codes stored in memory with 5-minute TTL; retrieved once and immediately deleted
  • Popup origin validation — only accepts messages from the expected origin
  • Token refresh margin — refreshes tokens 60 seconds before expiry
  • Rate limiting — built-in limiters for OAuth (10/min), result polling (30/min), and session (30/min) endpoints

Local Development

1. Install dependencies

# bun (recommended)
bun install

# pnpm
pnpm install

# npm
npm install

# yarn
yarn install

2. Available development scripts

# Type-check
bun run typecheck   # or: pnpm typecheck | npm run typecheck | yarn typecheck

# Run tests
bun test            # or: pnpm test | npm test | yarn test

# Build
bun run build       # or: pnpm build | npm run build | yarn build

# Watch mode
bun run dev         # or: pnpm dev | npm run dev | yarn dev

Supported localhost origins:

  • http://localhost:3000
  • http://localhost:5173
  • http://localhost:4173

You can also run the included demo app:

cd examples/venm-auth-demo
# Create a .env file with your OAuth credentials

# bun
bun install && bun run dev

# pnpm
pnpm install && pnpm dev

# npm
npm install && npm run dev

# yarn
yarn install && yarn dev

Error Handling

Client-side error codes:

| Code | Description | |------|-------------| | POPUP_BLOCKED | Browser blocked the popup | | POPUP_CLOSED | User closed the popup | | POPUP_TIMEOUT | Authentication timed out | | STATE_MISMATCH | OAuth state parameter mismatch (potential CSRF) | | PROVIDER_ERROR | OAuth provider returned an error | | UNAUTHORIZED | Invalid or expired token | | TIMEOUT | HTTP request timed out | | NO_REFRESH_TOKEN | No refresh token available for session refresh | | SESSION_EXPIRED | Auto-refresh failed — session expired and has been cleared |

Server-side error codes:

| Code | Description | |------|-------------| | MISSING_CODE | Authorization code missing from request | | TOKEN_EXCHANGE_FAILED | OAuth token exchange with provider failed | | USER_CREATE_FAILED | Failed to create or update user | | MISSING_TOKEN | No Bearer token in request | | SESSION_NOT_FOUND | Session not found in database | | USER_NOT_FOUND | User not found in database | | INVALID_TOKEN | Access token is invalid or expired | | INVALID_REFRESH_TOKEN | Refresh token is invalid or expired | | MISSING_REFRESH_TOKEN | Refresh token missing from request | | CSRF_INVALID_STATE | CSRF state parameter missing or mismatch | | RATE_LIMIT_EXCEEDED | Too many requests |

TypeScript

import type { User, Session, AuthState, ProviderType, SDKConfig, OAuthConfig } from "venm-auth";
import type { DatabaseAdapter, ServerSession, VenmAuthConfig } from "venm-auth/server";

Package Exports

| Import Path | Contents | |-------------|----------| | venm-auth | React components, hooks, context, services, types | | venm-auth/server | Express router, JWT utilities, database adapters, middleware | | venm-auth/components | React components only | | venm-auth/hooks | React hooks only |

Database Adapters

MongoDB (built-in)

import { createMongoDBAdapter } from "venm-auth/server";

const db = createMongoDBAdapter({
  uri: "mongodb://localhost:27017/myapp",
  databaseName: "myapp-auth",
});

The adapter creates a TTL index on refreshExpiresAt so expired sessions are automatically cleaned up by MongoDB. The access token expiry (expiresAt) is not TTL-indexed — sessions remain valid until the refresh token expires, allowing auto-refresh to revive them.

Custom Adapters

Implement the DatabaseAdapter interface for PostgreSQL, SQLite, Redis, etc.:

import type { DatabaseAdapter, CreateSessionData } from "venm-auth/server";

const myAdapter: DatabaseAdapter = {
  async createSession(data: CreateSessionData) {
    // data includes:
    //   accessToken, refreshToken, expiresAt,
    //   refreshExpiresAt  ← absolute timestamp for cleanup
    // ...
  },
  // ... implement all required methods
};

Note: The refreshExpiresAt field was added in v1.2.0 — custom adapters must include it in CreateSessionData for proper session cleanup.


License: MIT