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

@startsimpli/auth

v0.4.25

Published

Shared authentication package for StartSimpli Next.js apps

Readme

@startsimpli/auth

Shared authentication for every StartSimpli frontend. JWT lifecycle, React context, an authFetch with transparent 401-retry, Next.js server helpers, session-storage adapters for web and React Native, and an in-memory mock backend for tests/demos.

Drives auth in raise-simpli, market-simpli, trade-simpli, vault-web, and the examples/mobile-rn Expo demo on the in-progress react-native-enablement branch.

Subpath exports

The package ships TypeScript source — consumers bundle it directly. The package.json exports map is the contract:

| Import | Use from | Notes | |---|---|---| | @startsimpli/auth | anywhere (client-safe) | Re-exports ./client + ./types + ./utils (no next/headers). | | @startsimpli/auth/client | browser / RN | AuthProvider, useAuth, authFetch, session-storage factories, mock backend. | | @startsimpli/auth/server | Next.js server | getServerSession, requireAuth, createAuthMiddleware, withAuth, withRole, getTokenFromRequest. Uses next/headers — never import from a client component. | | @startsimpli/auth/token | React Native | DOM-free TokenAuthClient + SecureTokenStorage (Keychain/Keystore via expo-secure-store). | | @startsimpli/auth/components | browser | GoogleSignInButton, OAuthCallback, useOAuthCallback, OAuthConnectionCard. | | @startsimpli/auth/email | server | createEmailService (Resend, optional peer). | | @startsimpli/auth/types | anywhere | Shared types — Session, AuthUser, AuthConfig, CompanyRole. |

The native/web split for storage is resolved by Metro file extensions (secure-session-storage.native.ts / secure-token-storage.native.ts), not a ./native subpath. Import the same createSecureSessionStorage / SecureTokenStorage from @startsimpli/auth/client (or /token) and the bundler picks the right file. expo-secure-store is an optional peer; when the native module is missing, both adapters degrade to in-memory storage instead of crashing.

Quick start (Next.js)

// app/layout.tsx
'use client';

import { AuthProvider } from '@startsimpli/auth/client';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <AuthProvider
          config={{
            apiBaseUrl: process.env.NEXT_PUBLIC_API_URL!,
            loginPath: '/auth/signin',
          }}
        >
          {children}
        </AuthProvider>
      </body>
    </html>
  );
}
// any client component
'use client';

import { useAuth } from '@startsimpli/auth/client';

export function Header() {
  const { user, isAuthenticated, logout } = useAuth();
  if (!isAuthenticated) return null;
  return (
    <div>
      Hi {user!.firstName} · <button onClick={logout}>Sign out</button>
    </div>
  );
}

authFetch — token attach + 401-retry + base-URL resolution

authFetch is the only HTTP helper every app should use for authenticated calls. It:

  1. Pulls the current access token from storage (see below) and attaches Authorization: Bearer <token> if no header is set already.
  2. Resolves relative URLs against NEXT_PUBLIC_API_URL (or NEXT_PUBLIC_API_BASE_URL) via resolveAuthUrl.
  3. Sets credentials: 'include' by default so the refresh cookie travels.
  4. On 401, calls refreshAccessToken() once (concurrent 401s share one refresh promise) and retries the original request with the new token.
  5. If the refresh fails or the retried request is still 401, clears the stored token and fires notifySessionExpired() — which calls the callback registered via setOnSessionExpired (wired automatically by AuthProvider).
import { authFetch } from '@startsimpli/auth/client';

const res = await authFetch('/api/v1/me/');   // resolved to NEXT_PUBLIC_API_URL
if (res.ok) {
  const me = await res.json();
}

@startsimpli/api's FetchWrapper routes its own 401-after-refresh through the same notifySessionExpired sink so every consumer hits a single redirect path.

Refresh-token classification

refreshAccessToken distinguishes "session is dead" from "backend is sick":

| Status | Result | |---|---| | 401 / 403 / 400 | Clear the stored token, return null. | | 5xx / network error | Throw TransientRefreshError. Do NOT log the user out — the access token still lives, callers should surface a retry. | | 200 | Return the new access token; persist via setAccessToken. |

Storage adapters (SessionStorage)

Backends that own their session (the mock backend, the React Native TokenAuthClient, offline-first clients) need somewhere to persist it across reloads/app restarts. The shared contract:

interface SessionStorage {
  load(): Promise<Session | null>;
  save(session: Session): Promise<void>;
  clear(): Promise<void>;
}

Factories in @startsimpli/auth/client:

  • createMemorySessionStorage() — the safe default for SSR + tests.
  • createWebSessionStorage({ key?, storage? })localStorage by default; silently falls back to memory when no Storage is available.
  • createSecureSessionStorage(key?)Metro-resolved. On the web it delegates to createWebSessionStorage; on React Native it persists to the iOS Keychain / Android Keystore via expo-secure-store. Falls back to in-memory when the native module isn't in the build.
  • createRememberAwareSessionStorage(persistent, shouldRemember, transient?) — wraps two storages. When shouldRemember() is true at save time, the session goes to persistent and transient is cleared; otherwise the reverse. load always reads persistent, so a session restores only if the last save was "remembered".

The web Django client doesn't take a SessionStorage at all — its refresh token lives in an httpOnly cookie managed by the browser. The token-mode client (@startsimpli/auth/token) is the one that needs storage on RN.

React Native parity (TokenAuthClient)

// On RN (Expo)
import { TokenAuthClient, SecureTokenStorage } from '@startsimpli/auth/token';

const auth = new TokenAuthClient({
  apiBaseUrl: 'https://api.example.com',
  storage: new SecureTokenStorage(),
});

This is the same lifecycle as the web AuthClient (login, refresh, logout, getCurrentUser) but DOM-free: no cookies, no window/document. It opts into the backend's token mode via X-Auth-Mode: token and persists the refresh token through the injected TokenStorage. See examples/mobile-rn/App.tsx for the wiring used by the Expo demo.

Server helpers (Next.js)

// middleware.ts
import { createAuthMiddleware } from '@startsimpli/auth/server';

export const middleware = createAuthMiddleware({
  loginPath: '/auth/signin',
  publicPaths: ['/auth/signin', '/auth/signup', '/auth/forgot-password'],
});

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

The middleware uses a two-tier check: it lets the request through if either a fresh access cookie (auth_session / access_token) is present OR a refresh_token cookie is present (the client will refresh on mount). This avoids the every-30-minute redirect dance when the access JWT expires mid-session (see raise-simpli-qcw).

Route-handler guards

// app/api/me/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { withAuth } from '@startsimpli/auth/server';

export const GET = withAuth(async (req: NextRequest, token: string) => {
  const res = await fetch(`${process.env.API_BASE_URL}/api/v1/auth/me/`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  return NextResponse.json(await res.json());
});

withRole(requiredRole, getUserRole, handler) adds an owner > admin > member > viewer check on top. Both extract the token via getRequestToken(req) (NextRequest).

Framework-agnostic token extraction

getTokenFromRequest(req: Request) works on any Request-compatible object (Node, Edge, Bun). It checks Authorization: Bearer ... then falls back to an access_token cookie. It returns the raw token string without expiry validation — used by vault-web API routes that hand the token straight to a downstream Django call.

Server components

// app/dashboard/page.tsx
import { getServerSession } from '@startsimpli/auth/server';
import { redirect } from 'next/navigation';

export default async function Dashboard() {
  const session = await getServerSession(process.env.API_BASE_URL!);
  if (!session) redirect('/auth/signin');
  return <div>Welcome, {session.user.firstName}</div>;
}

validateSession(apiBaseUrl) and requireAuth(apiBaseUrl) are the shorter forms. refreshServerToken(apiBaseUrl) hits the refresh endpoint from a server context with cookies forwarded.

Mock backend (tests, Storybook, offline demos)

createMockAuthBackend() returns a full AuthBackend implementation — exactly what AuthProvider needs, with no network. Pair it with any SessionStorage to survive reloads.

import {
  AuthProvider,
  createMockAuthBackend,
  createWebSessionStorage,
} from '@startsimpli/auth/client';

const backend = createMockAuthBackend({
  accounts: [
    { email: '[email protected]', password: 'hunter2',
      user: { id: 'u1', email: '[email protected]', firstName: 'Demo',
              lastName: 'User', isEmailVerified: true,
              createdAt: '', updatedAt: '' } },
  ],
  storage: createWebSessionStorage(),
});

export default function App() {
  return <AuthProvider backend={backend}>{/* ... */}</AuthProvider>;
}

Pass backend instead of config and AuthProvider skips constructing the Django AuthClient entirely. The mock surface adds requestPasswordReset, resetPassword, requestEmailVerification, verifyEmail, and upsertAccount on top of the base contract — see packages/billing/src/mock for an example consumer.

Verification

pnpm --filter @startsimpli/auth test          # 140 tests across 17 files
pnpm --filter @startsimpli/auth type-check

The suite covers auth-client, authFetch retry, middleware, the mock backend, the secure-storage adapters (native + web), the token-auth core, permissions, and validation.

Browser verification is not optional. Any sign-in / sign-out / OAuth flow change MUST be driven through a localhost dev server with mcp__debugg-ai__check_app_in_browser — type-checks and unit tests do not prove a redirect chain works. See the MCP-default rule in /Users/qosha/Repos/start-simpli/CLAUDE.md (rule 10 / the "feedback_verify_ui_with_mcp" memory).

Shared-package policy

Per CLAUDE.md rule 9, every app-side auth helper that another app might plausibly want belongs here — not in apps/*/src. No new AuthProvider, no app-local authFetch wrapper, no per-app useAuth hook. Extend this package instead. The current consumers all go through the public surface above.

License

Private package for the StartSimpli monorepo.