npm package: @stacknet/userutils
Description: Reusable auth, billing, and security utilities for StackNet stacks and applications
Latest version: 0.5.1

# @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

```bash
pnpm add @stacknet/userutils
```

## Quick Start

### 1. Set up environment variables

```bash
# .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.

```typescript
// 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)
}
```

```typescript
// 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)
}
```

```typescript
// 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

```tsx
// 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>
  )
}
```

```tsx
// 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

```tsx
// 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

```tsx
// 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.

```typescript
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.

```typescript
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.

```tsx
<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.

```tsx
<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