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

@pixygon/auth

v1.6.0

Published

Shared authentication package for all Pixygon applications

Readme

@pixygon/auth

The shared account spine for every Pixygon web app — one Pixygon account, one token store, one set of branded forms. 16 repos depend on it, second only to @pixygon/analytics.

It talks to the central Pixygon API (https://api.pixygon.com/v1): register, verify, login, refresh, password recovery, /users/me. The Unity counterpart is com.pixygon.passport (related system, deliberately separate package).


Retrofitting it into an existing app (≈20 minutes)

Written for an agent. Do these in order.

1. Install

npm i @pixygon/auth@^1.6.0

>= 1.4.7 sends X-App-Id on every request (server-side per-app attribution for emails and Discord notices); 1.5.0 returns a soft-verification session on register, so a new user is logged in immediately instead of being stranded on a "check your email" screen; 1.6.0 adds the opt-in SSO client (nothing changes unless you set config.sso).

2. Know your ids

| Field | Value | |---|---| | appId | the MongoDB ObjectId from this repo's .pixygon.json | | appName | human name — appears in verification/recovery emails and Discord | | baseUrl | https://api.pixygon.com/v1 (with an env override) |

appId does two jobs: it namespaces the localStorage token keys (<appId>_access_token, …) and it is sent as X-App-Id so the server knows which app the request came from. Use the ObjectId, not a slug — a slug still isolates tokens but the server can't attribute it to a project.

⚠ Changing appId later logs everyone out (the storage keys move).

⚠ One exception: an app that deliberately shares the hub's session (e.g. PixygonAdmin) sets appId to the pixygon.io project 65481fec93ee5f866ade917f. Only do that on purpose.

3. Mount the provider

The estate pattern is a thin local wrapper (src/providers/AuthProvider.tsx) that owns the config and re-exports the hooks, so the rest of the app never imports the package directly:

// src/providers/AuthProvider.tsx  — copied from Tastebud
import { AuthProvider as PixygonAuthProvider, useAuth, useUser } from '@pixygon/auth'
import type { ReactNode } from 'react'

const AUTH_CONFIG = {
  baseUrl: import.meta.env.VITE_API_URL || 'https://api.pixygon.com/v1',
  appId: '69ee9b3f7a116f8ba90d0890',        // from .pixygon.json
  appName: 'Tastebud',
  recoveryBaseUrl: `${window.location.origin}/reset-password`,
}

export function AuthProvider({ children }: { children: ReactNode }) {
  return <PixygonAuthProvider config={AUTH_CONFIG}>{children}</PixygonAuthProvider>
}

export { useAuth, useUser }

Mount it outside the router and outside every other pearl provider — subscription, social and savedata all read the token from it:

<AuthProvider>
  <PixygonPlusProvider>      {/* @pixygon/subscription */}
    <SocialProvider>         {/* @pixygon/social       */}
      <SaveDataProvider>     {/* @pixygon/savedata     */}
        <BrowserRouter><App /></BrowserRouter>
      </SaveDataProvider>
    </SocialProvider>
  </PixygonPlusProvider>
</AuthProvider>

4. Use it

import { useAuth, useUser, useToken, useRequireAuth } from '@pixygon/auth'

const { login, register, logout, isAuthenticated, isLoading, error } = useAuth()
const { userName, email, subscriptionTier } = useUser()
const { accessToken, getAccessToken } = useToken()   // pass getAccessToken to the other pearls

// protected route
const { isAuthorized } = useRequireAuth({ roles: ['admin'], onUnauthorized: () => nav('/login') })

Or drop in the branded forms instead of writing your own:

import { PixygonAuth } from '@pixygon/auth/components'
<PixygonAuth mode="login" theme="dark" onSuccess={() => nav('/')} onModeChange={(m) => nav(`/auth/${m}`)} />

Modes: login | register | verify | forgot-password | recover-password. For recover-password, pass the email + code from the recovery link, and set recoveryBaseUrl in the config to the route that renders it.

5. Verify it worked

node node_modules/@pixygon/auth/verify.mjs
# or:
pearl verify auth

Static check — installed version, <AuthProvider> mounted, non-placeholder appId that agrees with .pixygon.json, baseUrl pointing at the real central API (localhost with no fallback fails). Exit 0 pass, 1 fail.

What verify cannot prove: that logging in works. It never touches the network. Auth is money-adjacent, so after any auth change also run the live end-to-end smoke against the real API — import-level tests have missed a prod-breaking register bug before:

API=https://api.pixygon.com/v1
U=smoke$RANDOM
# 1. register (1.5.0+ returns a session token straight away)
TOK=$(curl -s -X POST $API/auth/register -H 'Content-Type: application/json' \
  -d "{\"userName\":\"$U\",\"email\":\"[email protected]\",\"password\":\"Passw0rd!23\"}" \
  | node -pe 'JSON.parse(require("fs").readFileSync(0)).token')
# 2. use the token on an authenticated endpoint
curl -s $API/users/me -H "Authorization: Bearer $TOK" | head -c 200

Single sign-on (optional) — log in once, be logged in everywhere

Every Pixygon app already shares one identity (the same accounts, the same /v1/auth). What it lacked was one session: tokens live in localStorage keyed <appId>_access_token, which is per-origin by browser design, and most apps sit on their own apex (pixiel.ai, lonnlyst.no, kikortet.no …).

SSO closes that gap with an authorization-code + PKCE top-level redirect against the central auth origin. Contract: Dyson/docs/SSO_DESIGN.md.

GET  {baseUrl}/auth/sso/authorize ?app&redirect_uri&state&code_challenge[&prompt=none]
     -> 302 back to redirect_uri?code=<one-time>&state=<state>
POST {baseUrl}/auth/sso/token      { code, code_verifier, app }
     -> { token, refreshToken, expiresIn, user }   (same shape as /auth/login)
GET  {baseUrl}/auth/sso/logout    ?redirect_uri&state   (TOP-LEVEL navigation)
     -> 302 back to redirect_uri, central session revoked

Entirely opt-in. An app that doesn't set config.sso behaves exactly as it did before — same storage keys, same hooks, same everything. Upgrading the package changes nothing on its own.

Server status: the /auth/sso/* endpoints are being built in parallel against this same contract. Until they are live, startLogin() will redirect to a 404. Ship the client wiring behind a flag if that matters to you.

1. Turn it on

One key in the config you already have:

const AUTH_CONFIG = {
  baseUrl: import.meta.env.VITE_API_URL || 'https://api.pixygon.com/v1',
  appId: '69ee9b3f7a116f8ba90d0890',        // from .pixygon.json
  appName: 'Tastebud',
  sso: true,                                 // <- that's the whole retrofit
}

sso: true means: expose useSso(), and auto-finish an SSO callback when the app loads with ?code in the URL. Tune it with an object instead:

sso: {
  autoComplete: true,          // finish ?code callbacks on mount (default true)
  redirectUri: undefined,      // default: current origin + pathname
  cleanUrl: true,              // strip code/state from the address bar (default true)
}

The redirect_uri must be on the server's allow-list (same ownership rule as CORS), so a new domain needs registering server-side before it will work.

2. Send people to it

import { useSso, useAuth } from '@pixygon/auth'

function LoginButton() {
  const { startLogin, status, error } = useSso()
  return (
    <button onClick={() => startLogin()} disabled={status === 'starting'}>
      Continue with Pixygon
    </button>
  )
}

startLogin() navigates away — treat every line after it as unreachable. Keep your password form: SSO is an additional door, not a replacement.

3. (Optional) the silent check for returning visitors

To answer "is this person already logged in centrally?" without a click:

function SilentSsoProbe() {
  const { isAuthenticated, isLoading } = useAuth()
  const { enabled, trySilentLogin } = useSso()
  useEffect(() => {
    if (enabled && !isLoading && !isAuthenticated) trySilentLogin()
  }, [enabled, isLoading, isAuthenticated, trySilentLogin])
  return null
}

trySilentLogin() uses prompt=none: the auth origin either bounces back with a code (invisible, fast) or with error=login_required. It runs at most once per tab per app — the package marks the probe in sessionStorage, which is what stops a server that keeps answering login_required from becoming an infinite redirect loop. Don't hand-roll this with startLogin({ prompt: 'none' }) in an effect; that is exactly the loop.

What actually happens

First visit, never logged in anywhere startLogin() → authorize → no central session → the auth origin shows its login → user signs in → central pxg_sso cookie is set → 302 back with ?code → the provider exchanges it → tokens land in <appId>_access_tokenuseAuth() reports authenticated. The URL is cleaned; your app never sees a token in it.

Returning visit, already signed in at another Pixygon app trySilentLogin() (or startLogin()) → authorize sees the cookie → 302 straight back with ?code → exchanged on mount → signed in with no form and no click. Two fast redirects, no password.

Returning visit, no central session, prompt=none 302 back with error=login_requireduseSso().status === 'login_required' → render your normal login. Not an error, not a throw.

Any callback that doesn't match Wrong/absent state, a code with no request pending in this tab, a stale request (>10 min): nothing is exchanged, the stash is dropped, the URL is cleaned, and you get { status: 'error' }. completeSsoLogin never throws.

Honest limits

  • No silent cross-domain check without a top-level redirect. Hidden iframes need third-party cookies, which browsers block by default. prompt=none is fast and invisible, but it is still a real navigation — the page unloads and comes back. Don't call it mid-form.
  • Logout is not a kill switch. useSso().logout() ends the central session (no more silent grants) and clears this app's tokens. Other apps that already hold a token keep working until it expires (max 7d access) unless they log out themselves. Anyone promising "log out everywhere, instantly" is promising something this design does not deliver.
  • Logout is a page navigation, not a fetch. pxg_sso is SameSite=Lax and the auth origin is cross-site for every app (a different registrable domain even from *.pixygon.io), so the cookie is NOT sent on a cross-site fetch — a POST would arrive without it and revoke nothing while looking like it worked. ssoLogout() therefore clears local tokens first and then navigates top-level to the auth origin, where the cookie is first-party and IS sent. Expect the page to leave; the returned central: 'navigating' says exactly that rather than claiming a confirmed revocation it cannot observe.
  • Secure context required. PKCE uses crypto.subtle, which does not exist on plain http:// origins (localhost is fine). On an insecure origin startLogin() sets status: 'error' with a message saying so.
  • sessionStorage required. The verifier must survive the redirect without leaking to other tabs. If storage is blocked (some private modes), SSO fails closed and the password form still works.
  • Changing appId still logs everyone out — the storage keys move. SSO doesn't change that.

Verify it worked

There is no static check for this — verify.mjs reports adoption but cannot prove a redirect flow works. Drive it once, by hand:

  1. Click your SSO button. The address bar goes to …/v1/auth/sso/authorize?app=<your ObjectId>&redirect_uri=<your origin+path>&state=…&code_challenge=…. code_verifier must NOT be in that URL — only the challenge.
  2. In DevTools → Application → Session Storage, there is one pxg_sso_pending_<appId> entry while the redirect is in flight.
  3. After landing back: the address bar has no code/state (they were stripped via history.replaceState, so they're not in history or the Referer either), and Local Storage has <appId>_access_token, <appId>_refresh_token, <appId>_user, <appId>_expires_at — the exact same keys a password login writes.
  4. Network tab: exactly one POST /auth/sso/token, body { code, code_verifier, app }, response identical in shape to /auth/login.
  5. Reload. You stay logged in, and there is no second token exchange.
  6. Negative test: paste ?code=x&state=y onto your app URL by hand. Nothing is exchanged, the params are stripped, useSso().status === 'error'.

Imperative (non-React) use

import { startSsoLogin, completeSsoLogin, ssoLogout, hasSsoCallback } from '@pixygon/auth'

if (hasSsoCallback()) {
  const result = await completeSsoLogin({ appId, baseUrl })   // never throws
  // 'success' | 'login_required' | 'error' | 'no_callback'
}
await startSsoLogin({ appId, baseUrl, prompt: 'none' })
await ssoLogout({ baseUrl, appId })

API

| Export | Use | |---|---| | <AuthProvider config> | the mount; everything else needs it | | useAuth() | user, status, isAuthenticated, isLoading, error, login, register, logout, verify, forgotPassword, recoverPassword, changePassword, hasRole | | useUser() | user, userName, email, role, subscriptionTier, totalTokens | | useToken() | accessToken, refreshToken, getAccessToken, refreshTokens — this is what you hand to social/savedata/subscription | | useAuthStatus() | isIdle, isLoading, isAuthenticated, isUnauthenticated, isVerifying | | useRequireAuth({roles, onUnauthorized}) | route guards | | useProfileSync() | keeps the cached user fresh | | useSso() | enabled, status, error, startLogin, trySilentLogin, completeLogin, logout — central SSO (opt-in via config.sso) | | startSsoLogin, completeSsoLogin, ssoLogout, trySilentSsoLogin, hasSsoCallback | the same flow without React | | generateCodeVerifier, createCodeChallenge, generateState | PKCE primitives (Web Crypto, no deps) | | PixygonAuth, LoginForm, RegisterForm, VerifyForm, ForgotPasswordForm, RecoverPasswordForm | @pixygon/auth/components | | createAuthApiClient, createTokenStorage | non-React / imperative use |

AuthConfig

{
  baseUrl: string            // REQUIRED — https://api.pixygon.com/v1
  appId: string              // REQUIRED — .pixygon.json ObjectId
  appName?: string           // shown in emails + Discord
  language?: 'en' | 'no'     // auth UI + email language
  recoveryBaseUrl?: string   // where the recovery link lands
  autoRefresh?: boolean      // default true
  refreshThreshold?: number  // seconds before expiry (default 300)
  onLogin?, onLogout?, onTokenRefresh?, onError?
  storage?: AuthStorage      // default localStorage
  theme?: AuthTheme
  debug?: boolean
  sso?: boolean | {          // central SSO — omitted = off, nothing changes
    autoComplete?: boolean   //   finish ?code callbacks on mount (default true)
    redirectUri?: string     //   default: current origin + pathname
    cleanUrl?: boolean       //   strip code/state afterwards (default true)
  }
}

Gotchas

  • Hooks outside the provider silently no-opisAuthenticated stays false forever. If login "does nothing", check the provider is above the component in the tree.
  • appId is a storage namespace. Changing it, or using two different values in one app, splits the session in two.
  • Don't confuse it with the other pearls' appId. @pixygon/subscription and @pixygon/social take their own appId (a lowercase app slug like tastebud) — those are unrelated to the auth one and are usually declared in the same file.
  • Register returns a token in 1.5.0+ only. On older versions the app must send the user through the verify screen before it has a session.
  • SSO is off unless you ask for it. No config.sso means no redirect handling, no new storage, no behaviour change. useSso() still works — it reports enabled: false and its actions are no-ops, so a button can render behind that flag without ceremony.
  • Redux apps need a small sync component (see below) — the package is the source of truth, Redux is a mirror.
function AuthReduxSync({ children }) {
  const { user, accessToken, isAuthenticated } = useAuth()
  const dispatch = useDispatch()
  useEffect(() => {
    if (isAuthenticated && user && accessToken) dispatch(setLogin({ user, token: accessToken }))
    else if (!isAuthenticated) dispatch(setLogout())
  }, [isAuthenticated, user, accessToken, dispatch])
  return children
}

Publishing

npm run build && npm publish --access public

Apps consume a change here only once it is published1.6.0 (the SSO client) is committed but not yet on npm, so npm i @pixygon/auth@^1.6.0 in a consuming repo will resolve to the last published version until someone with a token runs the two commands above.

MIT — Pixygon