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

@threshold1/auth

v0.1.20

Published

Threshold1 Passkey Authentication SDK

Readme

@threshold1/auth

Passkey-first authentication SDK for modern web apps. Add passkey, OTP, and magic link auth with automatic fallback — in minutes. No backend required.

npm: @threshold1/auth
Docs: threshold1-docs.vercel.app
Dashboard: threshold1.phantomclick.in


How It Works

  1. Sign up at threshold1.phantomclick.in
  2. Get an API key from the dashboard
  3. Install the SDK
  4. Call auth.login(email) — threshold1 handles everything else

The SDK connects to our hosted production API by default. No baseUrl needed. No server to run.


Install

npm install @threshold1/auth

Quick Start

import { Threshold1 } from '@threshold1/auth'

// IMPORTANT: Initialize at module level — outside any component or function
// Initializing inside a component resets auth state on every render
const auth = new Threshold1({
  apiKey: 'th_live_xxxxxxxxxxxxxxxx',
  redirectUrl: 'https://yourapp.com',
  auth: {
    methods: ['passkey', 'otp', 'magic'],
    passkeyNotFound: 'register',           // create passkey on first login
    onAfterAuth: async (user, method) => { // link to your DB after every auth
      await myDB.upsert({ t1UserId: user.id, email: user.email })
    }
  }
})

await auth.register('[email protected]')       // register new user
await auth.login('[email protected]')          // login existing user
await auth.resumeSession()                  // call on every page load

Configuration

const auth = new Threshold1({
  apiKey: 'th_live_xxxxxxxxxxxxxxxx',      // Required
  redirectUrl: 'https://yourapp.com',      // Required for magic link
  auth: {
    methods: ['passkey', 'otp', 'magic'],  // Default: ['passkey', 'magic', 'otp']
    onFallback: (attempted, used) => {     // Called when SDK falls back
      console.log(`${attempted} failed, using ${used}`)
    },
    onAfterAuth: async (user, method) => { // Called after every successful auth
      await myDB.upsert({ t1UserId: user.id, email: user.email })
    },
    passkeyNotFound: 'fallback',           // 'fallback' | 'register' | 'register-strict'
  },
  debug: false                             // Set true for detailed console logs
})

Config options

| Option | Required | Description | |--------|----------|-------------| | apiKey | Yes | Your th_live_ key from the dashboard | | redirectUrl | For magic link | URL where users land after clicking the magic link email | | auth.methods | No | Auth method priority order. Default: ['passkey', 'magic', 'otp'] | | auth.onFallback | No | Called when SDK falls back. Args: (attempted: string, used: string) | | auth.onAfterAuth | No | Called after every successful auth. Args: (user: UserProfile, method: AuthMethod). Use to link threshold1 user to your database. | | auth.passkeyNotFound | No | 'fallback' (default) — go to OTP/magic. 'register' — create passkey on the spot, fall back if fails. 'register-strict' — create passkey or throw error. | | debug | No | Logs detailed auth flow to console. Development only. |


Methods

Core

await auth.register(email)      // Register new user — tries methods in order with fallback
await auth.login(email)         // Login existing user — tries methods in order with fallback
await auth.resumeSession()      // Call on every page load — handles magic link token in URL
await auth.getUser()            // Returns { id, email } — throws if not authenticated
await auth.getSession()         // Returns { userId, issuedAt, expiresAt }
await auth.logout()             // Invalidates session server-side, clears JWT from memory
auth.isAuthenticated            // Boolean — true if JWT in memory

Identity Bridge (externalUserId)

Connect threshold1 passkeys to your existing user IDs. Use this when you already have users in your own database.

// Register with your own user ID
await auth.register({ email: '[email protected]', externalUserId: 'your_user_123' })

// onAfterAuth receives your own user ID back
onAfterAuth: async (user, method) => {
  // user.externalUserId — your own user ID (null if not set)
  // user.id — threshold1 internal UUID
  const myUser = await myDB.findById(user.externalUserId)
}

Passkey Management

// Enroll a passkey for an already-authenticated user
// Call after login() via OTP or magic link to add passkey to their account
const passkey = await auth.addPasskey({ externalUserId: 'your_user_123', email: '[email protected]' })
// Returns { credentialId, createdAt, deviceHint }

// List all registered passkeys for current user
const passkeys = await auth.listPasskeys()
// Returns [{ credentialId, createdAt }]

// Remove a passkey by credential ID
await auth.removePasskey(passkey.credentialId)

Passkey-first login (no email)

// Show passkey picker without asking for email first
try {
  await auth.login() // no args — browser shows passkey picker
} catch (err) {
  // No passkey found or user cancelled — ask for email and fall back
  showEmailInput()
}

OTP with custom UI

// Send OTP to email
await auth.sendOtp(email)

// User enters code in your own input field
const user = await auth.verifyOtp(email, code)
// Returns { id, email } — JWT stored automatically

Individual methods (no fallback)

await auth.registerPasskey(email)    // Passkey only
await auth.loginWithPasskey(email)   // Passkey only — respects passkeyNotFound config
await auth.loginWithMagic(email)     // Magic link only
await auth.registerWithMagic(email)  // Magic link only

// Deprecated — use sendOtp + verifyOtp instead
await auth.loginWithOtp(email)       // Uses window.prompt() — blocked in iframes
await auth.registerWithOtp(email)    // Uses window.prompt() — blocked in iframes

Hooks

onAfterAuth

Fires after every successful authentication — register(), login(), resumeSession(). Use it to link threshold1 users to your own database.

onAfterAuth: async (user, method) => {
  // user.id             — stable threshold1 user ID
  // user.email          — user's email address
  // user.externalUserId — your own user ID (null if not set)
  // method              — 'passkey' | 'otp' | 'magic'
  await myDB.users.upsert({
    t1UserId: user.id,
    email: user.email,
    lastLoginMethod: method,
    lastSeenAt: new Date(),
  }, { onConflict: 'email' })
}

Note: If calling verifyOtp() directly (not through login()/register()), onAfterAuth does not fire automatically. Handle post-auth logic yourself in that case.

passkeyNotFound

Controls what happens when login() is called but the user has no passkey saved:

// 'fallback' (default) — go to next method in chain (OTP/magic)
passkeyNotFound: 'fallback'

// 'register' — try to create passkey on the spot, fall back if fails
passkeyNotFound: 'register'

// 'register-strict' — create passkey or throw error (no fallback)
passkeyNotFound: 'register-strict'

With 'register', users never need to explicitly sign up first — login handles both new and returning users automatically.


Magic Link Callback

// Call on every page load — required for magic link to work
useEffect(() => {
  auth.resumeSession()
    .then(user => { if (user) setCurrentUser(user) })
    .catch(err => {
      // Token expired or already used
      setError('Magic link expired. Please request a new one.')
    })
}, [])

Integration Patterns

Add passkey to existing app

threshold1 is headless — no UI components. Add a passkey button alongside your existing auth:

// Your existing Google/password buttons stay unchanged
// Add threshold1 for passkey/OTP/magic
async function handlePasskeyLogin() {
  await auth.login(email)
  const t1User = await auth.getUser()
  const myUser = await myDB.findByEmail(t1User.email)
  setCurrentUser(myUser)
}

Session persistence across page refresh

JWT is in memory only. Use onAfterAuth to save session to localStorage:

onAfterAuth: async (user, method) => {
  const session = await auth.getSession()
  localStorage.setItem('t1_session', JSON.stringify({
    userId: user.id, email: user.email, expiresAt: session.expiresAt
  }))
}

// On page load — restore saved session
const saved = localStorage.getItem('t1_session')
if (saved) {
  const s = JSON.parse(saved)
  if (new Date(s.expiresAt) > new Date()) {
    setUser({ id: s.userId, email: s.email })
  } else {
    localStorage.removeItem('t1_session')
  }
}

User management

threshold1 does not provide a user list API. You own your users — build your own users table using webhooks:

// Webhook handler — your server
if (event.event === 'user.registered') {
  await myDB.users.create({
    t1UserId: event.data.user_id,  // use as foreign key
    email: event.data.email,
  })
}

Passkey Setup

Production

  1. Go to dashboard → Authentication → Passkey RP Domain
  2. Add your production domain under Production Domain
  3. Add your production URL to Passkey Origins
  4. Deploy to HTTPS — done

Local Development with ngrok

Passkey requires HTTPS. ngrok creates an HTTPS tunnel to your local app.

brew install ngrok
ngrok http 3001
# Output: https://abc123.ngrok-free.dev -> http://localhost:3001

Setup (do once per ngrok URL):

  1. Add ngrok URL to Passkey Origins in dashboard
  2. Set ngrok URL as Test Domain in dashboard → Authentication → Passkey RP Domain
  3. Update redirectUrl in SDK config to ngrok URL
  4. Open your app via the ngrok URL — not localhost

Fix React hydration through ngrok (Next.js only):

Next.js HMR uses WebSocket which ngrok blocks, preventing button clicks from working. Add to next.config.ts:

const nextConfig: NextConfig = {
  allowedDevOrigins: ['abc123.ngrok-free.dev'],  // no https://
}

When ngrok URL changes (free tier changes on every restart):

  1. Update allowedDevOrigins in next.config.ts
  2. Update Passkey Origins in dashboard
  3. Update Test Domain in dashboard
  4. Update redirectUrl in SDK config

OTP and magic link work on http://localhost without ngrok.


Error Handling

import { Threshold1ApiError } from '@threshold1/auth'

try {
  await auth.login(email)
} catch (err) {
  if (err instanceof Threshold1ApiError) {
    console.error(err.message)  // Human-readable
    console.error(err.status)   // HTTP status
    console.error(err.code)     // Machine-readable — see table below
  }
}

Error codes

| Code | Meaning | Action | |------|---------|--------| | MISSING_API_KEY | No Authorization header | Check SDK initialization | | INVALID_FORMAT | API key format wrong | Check key starts with th_live_ | | API_KEY_NOT_FOUND | Key prefix not found | Check key is correct | | API_KEY_REVOKED | Key was revoked | Create new key in dashboard | | HASH_MISMATCH | Key prefix found but hash mismatch | Key may be corrupted — create new | | RATE_LIMIT_EXCEEDED | Too many requests or OTP sends | Slow down or wait | | MAU_LIMIT_EXCEEDED | Monthly user limit reached | Upgrade plan in dashboard | | INVALID_INPUT | Bad email or missing field | Check email format | | INVALID_CODE | Wrong or expired OTP | Request new OTP | | INVALID_TOKEN | Magic link expired or used | Request new magic link | | INVALID_SESSION | JWT expired or session deleted | User must log in again | | ORIGIN_NOT_ALLOWED | Domain not in Allowed Origins | Add domain in dashboard | | EMAIL_CONFLICT | Email already in use | Use different email | | INTERNAL_ERROR | Server error | Check dashboard status |


React Example

'use client'
import { useState, useEffect } from 'react'
import { Threshold1 } from '@threshold1/auth'

// Module level — NEVER inside the component function
const auth = new Threshold1({
  apiKey: process.env.NEXT_PUBLIC_T1_API_KEY!,
  redirectUrl: process.env.NEXT_PUBLIC_T1_REDIRECT_URL!,
  auth: {
    methods: ['passkey', 'otp', 'magic'],
    passkeyNotFound: 'register',
    onAfterAuth: async (user) => {
      await myDB.upsert({ t1UserId: user.id, email: user.email })
    }
  }
})

export default function AuthPage() {
  const [email, setEmail] = useState('')
  const [user, setUser] = useState(null)

  useEffect(() => {
    auth.resumeSession()
      .then(u => { if (u) setUser(u) })
      .catch(() => {})
  }, [])

  if (user) return (
    <div>
      <p>Welcome, {user.email}</p>
      <button onClick={async () => { await auth.logout(); setUser(null) }}>
        Logout
      </button>
    </div>
  )

  return (
    <div>
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
      <button onClick={async () => { await auth.login(email); setUser(await auth.getUser()) }}>
        Sign in
      </button>
    </div>
  )
}

Important Notes

Initialize at module level.
Always put const auth = new Threshold1({...}) outside any component function. Initializing inside a component resets auth state on every render.

Passkeys are domain-bound.
A passkey registered on yourapp.com cannot be used on staging.yourapp.com. Users need separate passkeys per domain. With passkeyNotFound: 'register', this happens automatically on first login from each domain.

Session tokens are in-memory only.
JWT is lost on page refresh. For persistent sessions, use onAfterAuth to save to localStorage and restore on page load.

API key is safe client-side.
Can only create auth sessions for your users. Cannot access other companies' data. Rotate from dashboard if compromised.

user.registered fires from login() too.
When login() is called with a new email, threshold1 creates the user and fires user.registered. Always use user.registered webhook to provision users — never assume it only comes from register().

Browser support.
Passkey: Chrome 108+, Safari 16+, Firefox 122+, Edge 108+. OTP and magic link work in all browsers.


Version

Current version: 0.1.15.


License

MIT