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

kandi-login

v0.1.1

Published

Universal OAuth login component for React — supports Tauri, Electron, and Web with MUI, Tailwind, or headless rendering

Readme

kandi-login

Multi-platform OAuth authentication framework. 5 client SDKs connect to 1 Node.js server SDK.

Supports Web (React), Electron, Tauri, iOS (Swift), and Android (Kotlin/Compose). Works with MUI, Tailwind, or headless rendering. Supports Apple, Google, Facebook, and Hello.coop OAuth providers.

npm install kandi-login

Architecture

┌─────────────────────────────────────┐
│           CLIENT SDKs               │
│                                     │
│  Web (React)     → Next.js, Vite   │
│  Electron        → Desktop          │
│  Tauri           → Desktop (Rust)   │
│  iOS (Swift)     → iPhone, iPad     │
│  Android         → Kotlin/Compose   │
└──────────────┬──────────────────────┘
               │  HTTPS
┌──────────────▼──────────────────────┐
│        NODE.JS SERVER SDK           │
│  kandi-login/server                 │
│  createAuthServer()                 │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│         YOUR DATABASE               │
└─────────────────────────────────────┘

The server SDK is framework-agnostic — it works with Next.js API routes, Express, Fastify, or any Node.js HTTP framework. The client SDK auto-detects the platform (Tauri, Electron, or Web) and uses the appropriate OAuth flow and token storage for each.


Integration Guide

This guide walks through a complete integration into a monorepo project with a Next.js frontend and API server. Adapt the framework-specific parts for Express, Fastify, Vite, etc.

Step 1: Install

npm install kandi-login

Peer dependencies (install only what you use):

# React (required for all client components)
npm install react react-dom

# MUI components (optional)
npm install @mui/material @emotion/react @emotion/styled

# Tauri (optional)
npm install @tauri-apps/api

Step 2: Server — Implement the UserAdapter

The server SDK needs 5 database functions. This is the only code you write on the server side — everything else is handled by createAuthServer.

// lib/user-adapter.ts
import type { UserAdapter, OAuthProfile } from 'kandi-login/server';

// Replace with your actual database client (Prisma, Drizzle, Supabase, etc.)
import { db } from './db';

export const userAdapter: UserAdapter = {
  // Called first: check if this provider+ID combo already exists
  async findByProviderId(provider, providerUserId) {
    return await db.user.findFirst({
      where: { [`${provider}_sub`]: providerUserId }
    });
  },

  // Called second: cross-provider account linking by email
  async findByEmail(email) {
    return await db.user.findFirst({ where: { email } });
  },

  // Called third: create new user if neither lookup found a match
  async createUser(profile: OAuthProfile) {
    return await db.user.create({
      data: {
        email: profile.email,
        name: profile.name,
        avatar_url: profile.avatarUrl,
        [`${profile.provider}_sub`]: profile.providerUserId,
      }
    });
  },

  // Called when an existing user (found by email) logs in with a new provider
  async linkProvider(userId, provider, providerUserId) {
    await db.user.update({
      where: { id: userId },
      data: { [`${provider}_sub`]: providerUserId }
    });
  },

  // Called during token refresh and validation to get fresh user data
  async getUserById(id) {
    return await db.user.findUnique({ where: { id } });
  },
};

The user object you return must have at least id and email. Additional fields like name, display_name, avatar_url, and role are included in the JWT access token if present.

Step 3: Server — Create the Auth Server

// lib/auth-server.ts
import { createAuthServer } from 'kandi-login/server';
import { userAdapter } from './user-adapter';

export const auth = createAuthServer({
  // JWT configuration (required)
  jwt: {
    secret: process.env.JWT_SECRET!,       // min 32 chars, used for HS256
    issuer: 'auth.yourapp.com',            // iss claim
    accessTokenTtl: '1h',                  // default: 1h
    refreshTokenTtl: '30d',                // default: 30d
  },

  // OAuth providers — configure only the ones you use
  providers: {
    // Hello.coop acts as an OIDC gateway to Google, Apple, etc.
    // One client ID covers all upstream providers via provider_hint
    hellocoop: {
      clientId: process.env.HELLO_CLIENT_ID!,
    },

    // Direct Google integration (for native mobile ID token flow)
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
    },

    // Apple (native ID token flow only, no server redirect)
    apple: {
      clientId: process.env.APPLE_CLIENT_ID!, // iOS Bundle ID or Services ID
      additionalClientIds: ['com.yourapp.web'], // optional: web Services ID
    },

    // Facebook requires a server redirect flow with app secret
    facebook: {
      appId: process.env.FACEBOOK_APP_ID!,
      appSecret: process.env.FACEBOOK_APP_SECRET!,
    },
  },

  // Your database adapter (required)
  userAdapter,

  // Your server's public URL (required for redirect flows)
  baseUrl: process.env.BASE_URL!,

  // Where to redirect after successful web login
  successRedirectUrl: process.env.SUCCESS_REDIRECT_URL!,

  // Lifecycle hooks (optional)
  onLogin: async (user, provider) => {
    // Called before token issuance. Throw to reject login.
    console.log(`${user.email} logged in via ${provider}`);
  },
  onUserCreated: async (user, provider) => {
    // Called after a new user is created
    console.log(`New user: ${user.email}`);
  },

  // Test personas (development only — see Testing section)
  enableTestPersonas: process.env.NODE_ENV !== 'production',
});

Step 4: Server — Mount the Route Handler

Next.js App Router:

// app/api/auth/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth-server';
import type { AuthRequest, AuthResponse } from 'kandi-login/server';

const routes: Record<string, { handler: typeof auth.handleLogin; methods: string[] }> = {
  login:    { handler: auth.handleLogin,    methods: ['GET'] },
  callback: { handler: auth.handleCallback, methods: ['GET'] },
  native:   { handler: auth.handleNativeLogin, methods: ['POST'] },
  refresh:  { handler: auth.handleRefresh,  methods: ['POST'] },
  validate: { handler: auth.handleValidate, methods: ['GET'] },
  logout:   { handler: auth.handleLogout,   methods: ['POST'] },
  // Test endpoints (only available when enableTestPersonas is true)
  ...(auth.handleSeedPersonas ? { 'test/seed': { handler: auth.handleSeedPersonas, methods: ['POST'] } } : {}),
  ...(auth.handleListPersonas ? { 'test/personas': { handler: auth.handleListPersonas, methods: ['GET'] } } : {}),
  ...(auth.handleLoginAs ? { 'test/login-as': { handler: auth.handleLoginAs, methods: ['POST'] } } : {}),
};

async function handle(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
  const { path } = await params;
  const route = routes[path.join('/')];
  if (!route) return NextResponse.json({ error: 'Not found' }, { status: 404 });
  if (!route.methods.includes(request.method)) return NextResponse.json({ error: 'Method not allowed' }, { status: 405 });

  const url = new URL(request.url);
  const query: Record<string, string> = {};
  url.searchParams.forEach((v, k) => { query[k] = v; });
  const headers: Record<string, string> = {};
  request.headers.forEach((v, k) => { headers[k] = v; });

  let body: unknown;
  if (request.method === 'POST') {
    try { body = await request.json(); } catch { body = {}; }
  }

  const authReq: AuthRequest = { method: request.method, url: request.url, query, headers, body };
  let statusCode = 200;
  let responseBody: unknown = null;
  let redirectUrl: string | null = null;
  const resHeaders: Record<string, string> = {};

  const authRes: AuthResponse = {
    status(code) { statusCode = code; return authRes; },
    json(data) { responseBody = data; },
    redirect(url) { redirectUrl = url; statusCode = 302; },
    send(data) { responseBody = data; },
    setHeader(name, value) { resHeaders[name] = value; return authRes; },
  };

  await route.handler(authReq, authRes);

  let response: NextResponse;
  if (redirectUrl) response = NextResponse.redirect(redirectUrl, statusCode as 301 | 302 | 303 | 307 | 308);
  else if (typeof responseBody === 'string') response = new NextResponse(responseBody, { status: statusCode });
  else response = NextResponse.json(responseBody ?? {}, { status: statusCode });

  for (const [k, v] of Object.entries(resHeaders)) response.headers.set(k, v);
  return response;
}

export const GET = handle;
export const POST = handle;
export async function OPTIONS() { return new NextResponse(null, { status: 204 }); }

Express:

// routes/auth.ts
import { Router } from 'express';
import { auth } from '../lib/auth-server';
import type { AuthRequest, AuthResponse } from 'kandi-login/server';

const router = Router();

function adapt(handler: typeof auth.handleLogin) {
  return async (req: any, res: any) => {
    const authReq: AuthRequest = {
      method: req.method,
      url: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
      query: req.query,
      headers: req.headers,
      body: req.body,
    };
    const authRes: AuthResponse = {
      status(code) { res.status(code); return authRes; },
      json(data) { res.json(data); },
      redirect(url) { res.redirect(url); },
      send(data) { res.send(data); },
      setHeader(name, value) { res.setHeader(name, value); return authRes; },
    };
    await handler(authReq, authRes);
  };
}

router.get('/login', adapt(auth.handleLogin));
router.get('/callback', adapt(auth.handleCallback));
router.post('/native', adapt(auth.handleNativeLogin));
router.post('/refresh', adapt(auth.handleRefresh));
router.get('/validate', adapt(auth.handleValidate));
router.post('/logout', adapt(auth.handleLogout));

export default router;
// Mount: app.use('/api/auth', authRouter);

Step 5: Server — Protect Your API Routes

Use auth.verifyToken() in middleware to protect routes:

// middleware/requireAuth.ts
import { auth } from '../lib/auth-server';

export async function requireAuth(req: any, res: any, next: any) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'No token' });

  try {
    const payload = await auth.verifyToken(token);
    req.user = payload; // { sub, email, role, display_name, avatar_url }
    next();
  } catch {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Step 6: Client — Configure and Wrap Your App

Define the config outside your component (or use useMemo) so the reference is stable:

// lib/auth-config.ts
import type { KandiLoginConfig } from 'kandi-login';

export const authConfig: KandiLoginConfig = {
  // Required: your auth server URL
  authServerUrl: process.env.NEXT_PUBLIC_AUTH_URL ?? 'http://localhost:3000',

  // Optional: separate API server for user profile endpoint
  // Defaults to authServerUrl if not set
  apiServerUrl: process.env.NEXT_PUBLIC_API_URL,

  // OAuth providers to show in the UI
  providers: [
    { id: 'google', name: 'Google' },
    { id: 'apple', name: 'Apple' },
    { id: 'facebook', name: 'Facebook' },
  ],

  // Desktop apps only: custom URL scheme for deep link callbacks
  // deepLinkScheme: 'myapp',

  // All paths are configurable (these are the defaults):
  // loginPath: '/api/auth/login',
  // refreshPath: '/api/auth/refresh',
  // profilePath: '/api/v1/users/me',
  // logoutPath: '/api/auth/logout',
  // webCallbackPath: '/auth/callback',
};

Wrap your app with AuthProvider:

// app/providers.tsx (or app/layout.tsx)
'use client';
import { AuthProvider } from 'kandi-login/react';
import { authConfig } from '@/lib/auth-config';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <AuthProvider
      config={authConfig}
      onLogin={(user) => console.log('Logged in:', user.email)}
      onLogout={() => console.log('Logged out')}
      onError={(err) => console.error('Auth error:', err)}
    >
      {children}
    </AuthProvider>
  );
}

Step 7: Client — Add Login UI

Choose one of three rendering options:

Option A: MUI Components

import { MuiLoginChip } from 'kandi-login/react/mui';
import { authConfig } from '@/lib/auth-config';

function Header() {
  return (
    <nav>
      <MuiLoginChip
        variant="glass"                           // 'glass' | 'flat'
        providers={authConfig.providers}
        showOverlayWhenUnauthenticated={true}      // auto-show login overlay
        appName="My App"
        menuItems={[
          { label: 'Profile', onClick: () => router.push('/profile') },
          { label: 'Settings', onClick: () => router.push('/settings'), position: 'bottom' },
        ]}
      />
    </nav>
  );
}

Option B: Tailwind Components

import { TailwindLoginChip } from 'kandi-login/tailwind';
import 'kandi-login/tailwind/kandi-login.css'; // CSS custom properties for theming
import { authConfig } from '@/lib/auth-config';

function Header() {
  return (
    <nav>
      <TailwindLoginChip
        variant="glass"
        providers={authConfig.providers}
        showOverlayWhenUnauthenticated={true}
        appName="My App"
      />
    </nav>
  );
}

Override the theme via CSS custom properties:

:root {
  --kl-primary: #667eea;
  --kl-text: #1a1a2e;
  --kl-bg: #ffffff;
  --kl-border: rgba(0,0,0,0.1);
  --kl-error: #ef4444;
  --kl-radius: 6px;
}

Option C: Headless (Bring Your Own UI)

import { HeadlessLoginChip } from 'kandi-login/react/headless';

function Header() {
  return (
    <HeadlessLoginChip>
      {({ user, isAuthenticated, login, logout, isLoading, menuOpen, openMenu, closeMenu }) => (
        isAuthenticated ? (
          <div>
            <button onClick={openMenu}>{user?.name}</button>
            {menuOpen && (
              <div>
                <button onClick={logout}>Logout</button>
              </div>
            )}
          </div>
        ) : (
          <button onClick={() => login('google')} disabled={isLoading}>
            Sign In
          </button>
        )
      )}
    </HeadlessLoginChip>
  );
}

Option D: Direct Hook (Full Control)

import { useAuth } from 'kandi-login/react';

function MyComponent() {
  const { user, isAuthenticated, isLoading, login, logout, getToken } = useAuth();

  if (isLoading) return <div>Loading...</div>;

  if (!isAuthenticated) {
    return <button onClick={() => login('google')}>Sign In with Google</button>;
  }

  return (
    <div>
      <p>Welcome, {user?.name}</p>
      <button onClick={logout}>Sign Out</button>
    </div>
  );
}

Step 8: Client — Make Authenticated API Calls

The AuthService (accessible via context) provides authenticatedFetch which automatically attaches the Bearer token and retries once on 401 after refreshing:

import { useAuth } from 'kandi-login/react';

function Dashboard() {
  const { getToken } = useAuth();

  async function fetchData() {
    const token = await getToken();
    const res = await fetch('/api/data', {
      headers: { Authorization: `Bearer ${token}` },
    });
    return res.json();
  }

  // ...
}

Server API Reference

Route Handlers

createAuthServer() returns an object with these handlers. Mount them at your preferred paths.

| Handler | Method | Path (suggested) | Description | |---|---|---|---| | handleLogin | GET | /api/auth/login | Initiates OAuth flow. Query: provider, return_url, client_type | | handleCallback | GET | /api/auth/callback | Receives OAuth redirect. Exchanges code, upserts user, issues tokens | | handleNativeLogin | POST | /api/auth/native | Native mobile ID token flow. Body: { provider, id_token } | | handleRefresh | POST | /api/auth/refresh | Rolling token refresh. Body: { refresh_token } | | handleValidate | GET | /api/auth/validate | Validates access token. Header: Authorization: Bearer <token> | | handleLogout | POST | /api/auth/logout | Acknowledges logout (stateless — tokens are not blacklisted) |

Utility Methods

| Method | Signature | Description | |---|---|---| | verifyToken | (token: string) => Promise<AccessTokenPayload> | Verify any access token for middleware use | | signTokens | (user: KandiLoginUser) => Promise<{ access_token, refresh_token }> | Sign tokens for custom flows |

UserAdapter Interface

You implement all 5 methods. The server SDK calls them in this order during login:

1. findByProviderId(provider, providerUserId) → user or null
2. findByEmail(email)                         → user or null (triggers linkProvider if found)
3. createUser(profile)                        → new user

| Method | Called When | |---|---| | findByProviderId(provider, id) | Every login — primary lookup | | findByEmail(email) | When findByProviderId returns null — cross-provider linking | | createUser(profile) | When both lookups return null — new user | | linkProvider(userId, provider, id) | When findByEmail matched — links new provider to existing user | | getUserById(id) | Token refresh and validation — fetches fresh user data |

JWT Access Token Payload

{
  sub: string;           // user ID
  email: string;
  role: string;          // from your user record
  display_name: string;
  avatar_url: string;
  aud: string;           // default: 'authenticated'
  iss: string;           // your configured issuer
  iat: number;
  exp: number;
}

Client API Reference

useAuth() Hook

const {
  user,            // KandiLoginUser | null
  isAuthenticated, // boolean
  isLoading,       // true during initial session restore
  error,           // string | null
  login,           // (provider?: string) => Promise<void>
  logout,          // () => Promise<void>
  refreshUser,     // () => Promise<void> — force-refresh from API
  getToken,        // () => Promise<string | null>
} = useAuth();

useLoginOverlay() Hook

For building custom overlay UIs:

const {
  showOverlay,  // true when not authenticated
  isLoggingIn,  // true during active login attempt
  error,        // string | null
  handleLogin,  // (provider?: string) => Promise<void>
  clearError,   // () => void
} = useLoginOverlay();

KandiLoginConfig

| Field | Type | Required | Default | |---|---|---|---| | authServerUrl | string | Yes | — | | apiServerUrl | string | No | authServerUrl | | providers | OAuthProviderConfig[] | Yes | — | | deepLinkScheme | string | No | — | | keychainService | string | No | 'kandi-login' | | tokenStorage | TokenStorageAdapter | No | auto-detected | | platformAdapter | PlatformAdapter | No | auto-detected | | loginPath | string | No | '/api/mobile/login' | | refreshPath | string | No | '/api/mobile/refresh' | | profilePath | string | No | '/api/v1/users/me' | | webCallbackPath | string | No | '/auth/callback' | | logoutPath | string | No | '/api/mobile/logout' |

Platform Behavior

| Platform | Detection | OAuth Flow | Token Storage | |---|---|---|---| | Web | Default | Popup window (fallback: redirect) | localStorage | | Tauri | window.__TAURI__ | In-app WebviewWindow + deep link | OS Keychain (via Rust) | | Electron | userAgent check | System browser + deep link | safeStorage (encrypted) | | iOS | Native SDK | ASWebAuthenticationSession | iOS Keychain | | Android | Native SDK | Chrome Custom Tabs + intent filter | EncryptedSharedPreferences |


Electron Setup

// main.ts
import { app, BrowserWindow, shell, ipcMain, safeStorage } from 'electron';

const PROTOCOL = 'myapp';
app.setAsDefaultProtocolClient(PROTOCOL);

// Single instance lock (required for deep links on Windows/Linux)
if (!app.requestSingleInstanceLock()) { app.quit(); }

// Handle deep link callback
function handleDeepLink(url: string) {
  const parsed = new URL(url);
  const accessToken = parsed.searchParams.get('access_token');
  const refreshToken = parsed.searchParams.get('refresh_token');
  const error = parsed.searchParams.get('error');

  if (error) mainWindow?.webContents.send('oauth-error', { error });
  else mainWindow?.webContents.send('oauth-callback', { access_token: accessToken, refresh_token: refreshToken });
}

app.on('open-url', (_, url) => handleDeepLink(url));               // macOS
app.on('second-instance', (_, argv) => {                            // Windows/Linux
  const url = argv.find(arg => arg.startsWith(`${PROTOCOL}://`));
  if (url) handleDeepLink(url);
});

// Expose APIs to renderer via preload
ipcMain.handle('open-external', (_, url) => shell.openExternal(url));
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
  openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
  onOAuthCallback: (cb: (data: any) => void) => {
    ipcRenderer.on('oauth-callback', (_, data) => cb(data));
  },
  onOAuthError: (cb: (data: any) => void) => {
    ipcRenderer.on('oauth-error', (_, data) => cb(data));
  },
  secureStorage: {
    get: (key: string) => ipcRenderer.invoke('secure-get', key),
    set: (key: string, value: string) => ipcRenderer.invoke('secure-set', key, value),
    delete: (key: string) => ipcRenderer.invoke('secure-delete', key),
  },
});

Tauri Setup

  1. Add the deep link scheme to tauri.conf.json:
{
  "app": {
    "security": {
      "dangerousRemoteDomainIpcAccess": [
        { "domain": "wallet.hello.coop", "enableTauriAPI": false, "windows": ["login"] }
      ]
    }
  }
}
  1. Implement the Rust commands (see tauri-plugin/commands.rs.template for a starter):
// src-tauri/src/commands/auth.rs
#[tauri::command]
pub async fn start_oauth(app: tauri::AppHandle, provider: String) -> Result<(), String> {
    // Create WebviewWindow, intercept deep link navigation
}

#[tauri::command]
pub async fn get_token(service: String, key: String) -> Result<Option<String>, String> {
    // Read from OS keychain (use keyring crate)
}

#[tauri::command]
pub async fn store_token(service: String, key: String, value: String) -> Result<(), String> {
    // Write to OS keychain
}

#[tauri::command]
pub async fn clear_tokens(service: String) -> Result<(), String> {
    // Delete from OS keychain
}

Testing

Test Personas

Enable test personas for development and integration testing:

const auth = createAuthServer({
  // ... your config
  enableTestPersonas: true, // NEVER enable in production
});

This exposes three additional endpoints:

| Endpoint | Method | Description | |---|---|---| | /test/seed | POST | Creates test users in your real database | | /test/personas | GET | Lists available test personas | | /test/login-as | POST | Returns real JWT tokens for a persona |

# Seed test users
curl -X POST http://localhost:3000/api/auth/test/seed

# List personas
curl http://localhost:3000/api/auth/test/personas

# Get tokens for a test user
curl -X POST http://localhost:3000/api/auth/test/login-as \
  -H 'Content-Type: application/json' \
  -d '{"personaId": "alice"}'

Warning: Test endpoints issue real JWT tokens with no authentication. They must be protected by network-layer controls or disabled entirely in production.

Built-in Personas

| ID | Name | Email | Role | |---|---|---|---| | alice | Alice Tester | [email protected] | admin | | bob | Bob Developer | [email protected] | user | | charlie | Charlie Viewer | [email protected] | viewer | | diana | Diana Manager | [email protected] | manager |


Environment Variables

# Required
JWT_SECRET=                     # min 32 chars, HS256 signing key
BASE_URL=https://yourapp.com    # your server's public URL

# OAuth Providers (configure the ones you use)
HELLO_CLIENT_ID=                # Hello.coop client ID
GOOGLE_CLIENT_ID=               # Google OAuth client ID
APPLE_CLIENT_ID=                # Apple Services ID or Bundle ID
FACEBOOK_APP_ID=                # Facebook App ID
FACEBOOK_APP_SECRET=            # Facebook App Secret

# Optional
JWT_ISSUER=auth.yourapp.com     # JWT issuer claim
SUCCESS_REDIRECT_URL=           # where to redirect after web login

CLI

npx kandi-login init    # Interactive setup wizard
npx kandi-login         # Development diagnostic tool

The init wizard generates:

  • .env.kandi-login with all required environment variable stubs
  • src/auth-config.ts with a complete client configuration

The dev tool provides:

  • Auth server connection testing
  • OAuth flow testing
  • Environment validation
  • Tauri Rust command generation

Entry Points

| Import | Contents | |---|---| | kandi-login | Core + React (convenience re-export) | | kandi-login/core | Platform detection, token storage, AuthService, platform adapters | | kandi-login/react | AuthProvider, useAuth, useLoginOverlay | | kandi-login/react/mui | MuiLoginChip, MuiLoginOverlay, MuiUserAvatar, icons | | kandi-login/react/headless | HeadlessLoginChip (render props) | | kandi-login/tailwind | TailwindLoginChip, TailwindLoginOverlay | | kandi-login/server | createAuthServer, JWT utils, security utils, test personas | | kandi-login/tailwind/kandi-login.css | CSS custom properties for Tailwind theming |


Known Limitations (v0.1.0)

  • Stateless logout: Tokens are HS256 with no server-side blacklist. handleLogout acknowledges the request but tokens remain valid until expiration. Clients must discard stored tokens.
  • Token delivery: Web redirect and desktop deep link flows deliver tokens via URL query parameters. Strip tokens from browser history with window.history.replaceState after receipt.
  • React peer dependency: The package lists react and react-dom as peer dependencies even for server-only usage (kandi-login/server). You can safely ignore the peer dependency warnings.

License

MIT