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

@blacktrench/auth-js

v1.0.0

Published

Client SDK for the Black Trench auth service — supabase-js compatible auth surface

Readme

@blacktrench/auth-js

Client SDK for the Black Trench Auth service — a multi-project authentication service with a supabase-js compatible surface. If you have code written against supabase.auth.*, the call sites, response envelopes, and auth events port 1:1.

Two entry points:

| Import | Runs in | Key used | Purpose | |---|---|---|---| | @blacktrench/auth-js | Browser (SPA) | Publishable key (bt_pub_…) | Sign-in flows, session management, auto-refresh | | @blacktrench/auth-js/server | Server (Node ≥ 18, Bun, edge) | Secret key (bt_secret_…) | Validate access tokens, fetch the user |

npm install @blacktrench/auth-js

Core concepts

  • Auth service URL — the base URL of the auth service (e.g. https://auth.blacktrench.studio/api). All requests go to <url>/auth/v1/*.
  • Project — each consuming app is a project on the auth service. The project is resolved from the API key sent with every request (apikey header).
  • Publishable key (bt_pub_<project-slug>_<random>) — safe to ship in browser bundles. Identifies the project; grants no privileged access.
  • Secret key (bt_secret_…) — server-side only. Never expose it in client code.
  • Session{ access_token, refresh_token, token_type: 'bearer', expires_in, expires_at, user }. The access token is an ES256 JWT (~short-lived); the refresh token is opaque and rotates on every refresh.
  • Sign-in flows — passwordless only: email OTP (6-digit code), magic link, and OAuth (e.g. Google). There is no password sign-in. Magic link and OAuth complete via the implicit flow: the service redirects back to your app with tokens in the URL fragment (#access_token=…), which the client picks up automatically.
  • Redirect allow-list — every redirectTo / emailRedirectTo URL must be allow-listed in the project's settings on the auth dashboard (wildcards supported: * doesn't cross / or ., ** crosses everything). ⚠️ A redirect that is not on the list is not an error — the service silently falls back to the project's Site URL (same semantics as Supabase). If your login "works but lands on the wrong page", this is why.

Response envelope (never throws)

Every method resolves to { data, error } — network and API failures are returned, not thrown:

const { data, error } = await client.auth.getUser()
if (error) {
  // error is an AuthError: { message: string, status?: number, code?: string }
  // error.code === 'network_error' → request never reached the service (status 0)
}

Error messages use Supabase's English phrasing (e.g. Token has expired or is invalid, Signups not allowed), so regex-based error translation written for Supabase keeps working.

Browser client

Setup

import { createAuthClient } from '@blacktrench/auth-js'

export const client = createAuthClient(
  import.meta.env.VITE_AUTH_URL,       // e.g. https://auth.blacktrench.studio/api
  import.meta.env.VITE_AUTH_PUB_KEY,   // bt_pub_...
)
// client.auth is the supabase-js-shaped auth interface

Create one client per app (module singleton). On construction the client:

  1. Checks the URL fragment for implicit-flow tokens (#access_token=…) — if present, strips them from the address bar, validates them against the service, saves the session, and emits SIGNED_IN.
  2. Otherwise restores the persisted session from localStorage (refreshing it first if expired).
  3. Starts a background refresh timer (checks every 30s, refreshes when the access token is within 90s of expiry).

An invalid/empty URL or key does not throw at construction — failures surface on the first call.

Options (all optional)

createAuthClient(url, publishableKey, {
  persistSession: true,       // persist to storage (default true)
  autoRefreshToken: true,     // background refresh + refresh-on-demand (default true)
  detectSessionInUrl: true,   // pick up #access_token fragments (default true)
  storageKey: 'custom-key',   // default: 'bt-<project-slug>-auth-token' (derived from the publishable key)
  storage: customAdapter,     // { getItem, setItem, removeItem } — default: localStorage, in-memory fallback
  fetch: customFetch,         // custom fetch implementation
})

Email OTP / magic link

// 1. Send the email (contains BOTH a 6-digit code and a magic link)
const { error } = await client.auth.signInWithOtp({
  email: '[email protected]',
  options: {
    emailRedirectTo: window.location.origin, // where the magic link lands (must be allow-listed)
    shouldCreateUser: true,                  // set false to reject emails without an account
  },
})

// 2a. User types the 6-digit code → verify it
const { data, error } = await client.auth.verifyOtp({
  email: '[email protected]',
  token: '123456',
  type: 'email',
})
// data.session / data.user are set on success; SIGNED_IN is emitted

// 2b. OR the user clicks the magic link → redirected to emailRedirectTo with
// #access_token=… in the URL; the client handles it automatically on load
// (detectSessionInUrl) and emits SIGNED_IN. No code needed.

OAuth (e.g. Google)

await client.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: window.location.origin,  // must be allow-listed
    // scopes: 'extra scopes',
    // queryParams: { prompt: 'select_account' },
    // skipBrowserRedirect: true,        // don't navigate; just return data.url
  },
})
// Performs a full-page redirect to the provider. After consent the service
// redirects back to redirectTo with #access_token=… — handled automatically
// on load, SIGNED_IN is emitted.

Auth state (drive your UI from this)

const { data: { subscription } } = client.auth.onAuthStateChange((event, session) => {
  // event: 'INITIAL_SESSION' | 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED'
  // session: Session | null
})
subscription.unsubscribe()
  • Every subscriber receives INITIAL_SESSION (asynchronously) once initialization settles — use it to resolve your app's loading state. session is null if the user is signed out.
  • TOKEN_REFRESHED fires on background refreshes and cross-tab session updates.
  • SIGNED_OUT fires on signOut(), when the refresh token is rejected, and when another tab signs out.
  • Sessions sync across tabs automatically (via storage events).

Session access

// Current session (refreshes first if expired) — local, fast, use for tokens:
const { data: { session } } = await client.auth.getSession()
const accessToken = session?.access_token   // send as Authorization: Bearer <token> to YOUR api

// Authoritative user check (validates against the auth service):
const { data: { user }, error } = await client.auth.getUser()

// Manual refresh (rarely needed — auto-refresh covers normal usage):
await client.auth.refreshSession()

// Sign out (revokes server-side, clears storage, emits SIGNED_OUT):
await client.auth.signOut()

getSession() returns { session: null } (with error: null) when signed out or when the stored session could not be refreshed. On network failures it keeps and returns the existing session so a flaky connection doesn't log the user out.

React integration pattern

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [session, setSession] = useState<Session | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const { data: { subscription } } = client.auth.onAuthStateChange((_event, session) => {
      setSession(session)
      setLoading(false) // INITIAL_SESSION always arrives, even when signed out
    })
    return () => subscription.unsubscribe()
  }, [])

  if (loading) return <Spinner />
  return <AuthContext.Provider value={{ session }}>{children}</AuthContext.Provider>
}

Calling your own backend

The auth service authenticates users; your API authorizes them. Attach the access token to your requests:

const { data: { session } } = await client.auth.getSession()
await fetch('/api/things', {
  headers: { Authorization: `Bearer ${session?.access_token}` },
})

Server client (validating tokens in your backend)

import { createServerClient } from '@blacktrench/auth-js/server'

const auth = createServerClient(process.env.AUTH_URL, process.env.AUTH_SECRET_KEY) // bt_secret_...

// In your request handler:
const jwt = request.headers.get('authorization')?.replace(/^Bearer /, '')
const { data: { user }, error } = await auth.auth.getUser(jwt)
if (error || !user) return new Response('Unauthorized', { status: 401 })
// user.id is the stable user UUID — key your own tables on it

Alternative — local JWT verification without a network round-trip: access tokens are ES256 JWTs; public keys are served at <url>/auth/v1/.well-known/jwks.json. Verify with jose:

import { createRemoteJWKSet, jwtVerify } from 'jose'
const jwks = createRemoteJWKSet(new URL(`${AUTH_URL}/auth/v1/.well-known/jwks.json`))
const { payload } = await jwtVerify(jwt, jwks) // payload.sub = user id

Use getUser() when you need revocation-awareness (a signed-out session fails immediately); use JWKS verification when latency matters (tokens remain valid until expiry).

Types (full public surface)

// '@blacktrench/auth-js'
function createAuthClient(url: string, publishableKey: string, options?: AuthClientOptions): { auth: Auth }

class Auth {
  signInWithOtp(credentials: SignInWithOtpCredentials): Promise<AuthResponse<{ user: null; session: null }>>
  verifyOtp(params: VerifyOtpParams): Promise<AuthResponse<{ user: User | null; session: Session | null }>>
  signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<AuthResponse<{ provider: string; url: string }>>
  onAuthStateChange(cb: (event: AuthChangeEvent, session: Session | null) => void): { data: { subscription: Subscription } }
  getSession(): Promise<AuthResponse<{ session: Session | null }>>
  getUser(jwt?: string): Promise<AuthResponse<{ user: User | null }>>
  refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse<{ session: Session | null; user: User | null }>>
  signOut(): Promise<{ error: AuthError | null }>
}

type AuthChangeEvent = 'INITIAL_SESSION' | 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED'
interface AuthResponse<T> { data: T; error: AuthError | null }
class AuthError extends Error { status?: number; code?: string }

interface Session {
  access_token: string      // ES256 JWT
  refresh_token: string     // opaque, single-use (rotates on refresh)
  token_type: 'bearer'
  expires_in: number        // seconds
  expires_at: number        // unix epoch seconds
  user: User
}

interface User {
  id: string                // stable UUID — use as the foreign key in your app
  aud: string
  role: string
  email: string | null
  email_confirmed_at: string | null
  app_metadata: { provider?: string | null; providers?: string[] }
  user_metadata: { name?: string; full_name?: string; avatar_url?: string; picture?: string; email?: string }
  identities?: UserIdentity[]   // one per linked provider (google, email, …)
  created_at: string
}

// '@blacktrench/auth-js/server'
function createServerClient(url: string, secretKey: string): {
  auth: { getUser(jwt: string): Promise<{ data: { user: User | null }; error: AuthError | null }> }
}

HTTP endpoints used (for debugging)

All calls send the API key as the apikey header against <url>/auth/v1:

| Method call | HTTP request | |---|---| | signInWithOtp | POST /otp | | verifyOtp | POST /verify | | signInWithOAuth | navigate to GET /authorize?provider=…&apikey=… | | (magic link click) | GET /verify?token=… → 302 to your app with #access_token=… | | token refresh | POST /token?grant_type=refresh_token | | getUser | GET /user (with Authorization: Bearer) | | signOut | POST /logout |

Gotchas & troubleshooting

  • Login redirects to the wrong URL → your redirectTo/emailRedirectTo is not on the project's redirect allow-list; the service silently fell back to the Site URL. Add the URL (with wildcards if needed) in the dashboard.
  • Session not restored after redirect → the tokens arrive in the URL fragment (#access_token=…). Make sure the client is constructed on the page the user lands on, before anything rewrites location, and that detectSessionInUrl isn't disabled. The fragment is stripped from the address bar immediately after being read.
  • Using the secret key in the browser (or the publishable key on the server) → key types are not interchangeable: bt_pub_… for the browser client, bt_secret_… for createServerClient.
  • getUser() returns Auth session missing! (code: 'session_missing') → no active session and no jwt argument was passed.
  • SSR → the default storage falls back to in-memory when localStorage is unavailable, so importing the client on the server is safe, but sessions only persist in the browser. Do sign-in flows client-side; validate tokens server-side with @blacktrench/auth-js/server.
  • Multiple tabs → handled: sessions sync via storage events, and refresh-token rotation has a grace window on the service to absorb concurrent refreshes.

Compatibility with supabase-js

Supported and drop-in: signInWithOtp, verifyOtp, signInWithOAuth, onAuthStateChange, getSession, getUser, refreshSession, signOut, the { data, error } envelopes, the event lifecycle, localStorage persistence, and implicit-flow fragment handling. Not implemented: password auth, phone auth, updateUser, admin APIs, and PKCE on the client (the service supports PKCE; the SDK uses the implicit flow).