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

@stacknet/userutils

v0.4.1

Published

Reusable auth, billing, and security utilities for StackNet stacks and applications

Readme

@stacknet/userutils

Authentication, session management, and billing for StackNet apps.

Google One Tap, OAuth (Google, Discord, Telegram, X), wallet login (Solana, Ethereum), cross-domain SSO via auth bridge, CSRF protection, and billing/subscription hooks.

Install

pnpm add @stacknet/userutils

Quick Start

1. Set up environment variables

# .env.local
AUTH_SECRET=your-hmac-secret-min-32-chars
NEXT_PUBLIC_STACK_ID=stk_your_stack_id
NEXT_PUBLIC_STACKNET_URL=https://stacknet.magma-rpc.com

2. Create API routes

Three server-side routes are required. These handle JWT signing, session validation, and logout — keeping secrets off the client.

// app/api/auth/callback/route.ts
import { createAuthCallback } from '@stacknet/userutils/server'

const handler = createAuthCallback({
  authSecret: process.env.AUTH_SECRET!,
  stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL || 'https://stacknet.magma-rpc.com',
  stackId: process.env.NEXT_PUBLIC_STACK_ID!,
  secureCookies: process.env.NODE_ENV === 'production',
})

export async function POST(request: Request) {
  return handler(request)
}
// app/api/auth/session/route.ts
import { createSessionHandler } from '@stacknet/userutils/server'

const handler = createSessionHandler({
  authSecret: process.env.AUTH_SECRET!,
  secureCookies: process.env.NODE_ENV === 'production',
})

export async function GET(request: Request) {
  return handler(request)
}
// app/api/auth/logout/route.ts
import { createLogoutHandler } from '@stacknet/userutils/server'

const handler = createLogoutHandler({
  stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL || 'https://stacknet.magma-rpc.com',
  secureCookies: process.env.NODE_ENV === 'production',
})

export async function POST(request: Request) {
  return handler(request)
}

3. Wrap your app with the provider

// components/auth-provider.tsx
'use client'

import { UserUtilsProvider } from '@stacknet/userutils/components'

const config = {
  apiBaseUrl: '',  // same-origin — hooks call your /api/auth/* routes
  stackId: process.env.NEXT_PUBLIC_STACK_ID,
  stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL,
  theme: 'dark',
}

export function AuthProvider({ children }) {
  return (
    <UserUtilsProvider config={config}>
      {children}
    </UserUtilsProvider>
  )
}
// app/layout.tsx
import { AuthProvider } from '../components/auth-provider'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  )
}

4. Add a connect page

// app/connect/page.tsx
'use client'

import { ConnectWidget } from '@stacknet/userutils/components'

export default function ConnectPage() {
  return (
    <ConnectWidget
      config={{
        apiBaseUrl: '',
        stackId: process.env.NEXT_PUBLIC_STACK_ID,
        stacknetUrl: process.env.NEXT_PUBLIC_STACKNET_URL,
      }}
      onSuccess={() => { window.location.href = '/' }}
      showWallets={['phantom', 'metamask']}
      showOTP={false}
    />
  )
}

5. Show auth state in your header

// components/header.tsx
'use client'

import { useSession, useStackAuth } from '@stacknet/userutils/hooks'
import Link from 'next/link'

export function Header() {
  const { isAuthenticated, loading } = useSession()
  const { session, logout, wallet } = useStackAuth()

  const displayAddress = wallet.address
    ? `${wallet.address.slice(0, 4)}...${wallet.address.slice(-4)}`
    : session?.userId?.slice(0, 8) ?? ''

  if (loading) return <div>Loading...</div>

  if (isAuthenticated) {
    return (
      <div>
        <span>{displayAddress}</span>
        <button onClick={logout}>Logout</button>
      </div>
    )
  }

  return <Link href="/connect">Log in</Link>
}

The JWT auto-refreshes when /api/auth/session is called and the token is within 5 minutes of expiry.

Hooks

useSession()

Reads the public session cookie. No network calls.

const { session, loading, isAuthenticated, refresh, readSession } = useSession()

// session: { userId, address, chain, expiresAt, planId, authMethod } | null
// isAuthenticated: boolean (session exists and not expired)

useStackAuth(config?)

Full auth engine — wallet connection, challenge/sign/verify, logout, bridge.

const {
  session,              // PublicSession | null
  isAuthenticated,      // boolean
  wallet,               // { connected, address, chain, provider }
  loading,              // boolean (auth in progress)
  error,                // string | null
  authenticateSolana,   // (provider?: 'phantom' | 'solflare') => Promise<boolean>
  authenticateEVM,      // () => Promise<boolean>
  authenticateOTP,      // (code: string) => Promise<boolean>
  logout,               // () => Promise<void>
  refresh,              // () => Promise<any>
  stackId,              // effective stack ID
  bridge,               // { ready, known, identity, identityCount, resolvedStackId }
} = useStackAuth({
  apiBaseUrl: '',
  stackId: 'stk_...',
  stacknetUrl: 'https://stacknet.magma-rpc.com',
  autoConnect: false,   // true = auto-login if bridge has known identity
})

Components

ConnectWidget

Drop-in wallet connect UI.

<ConnectWidget
  config={config}           // UserUtilsConfig (required)
  onSuccess={() => {}}      // Called after successful auth
  title="Connect"           // Heading text
  showWallets={['phantom', 'metamask']}  // Which wallets to show
  showOTP={false}           // Show access code option
  className=""              // CSS classes
/>

UserUtilsProvider

React context provider. Wrap your app with this.

<UserUtilsProvider config={config} callbacks={callbacks?}>
  {children}
</UserUtilsProvider>

Security

  • HttpOnly cookies prevent XSS token theft
  • SameSite=Lax prevents CSRF on state-changing requests
  • Secure flag enforces HTTPS in production
  • CSRF double-submit validates cookie matches header on mutations
  • Short JWT expiry (15 min) with transparent auto-refresh
  • Rate limiting on auth callback (10 req/min per IP)
  • Constant-time comparison for JWT signature validation

License

MIT