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

@faable/auth-js

v2.1.0

Published

<p align="center"> <a href="https://faable.com"> <img src="https://www.faable.com/assets/logo/Emblem.png" height="96"> <h3 align="center">Faable</h3> </a> </p>

Downloads

1,407

Readme

Features

  • OAuth social connections (Google, GitHub, …) with PKCE and implicit flows
  • Username + password login
  • Passwordless: email magic link and OTP code
  • Automatic token refresh with cross-tab synchronization via BroadcastChannel
  • Pluggable storage adapters (localStorage, cookies, or custom)
  • Server-side session helpers for Next.js

Install

npm install @faable/auth-js

Requires Node.js >=22.8 for development. The published bundle runs in any modern browser and in Node/SSR environments.

Quick start

import { createClient } from '@faable/auth-js'

export const auth = createClient({
  domain: '<faableauth_domain>',
  clientId: '<client_id>',
  redirectUri: window.location.origin
})

// Trigger a social login
await auth.signInWithOauthConnection({ connection: 'google' })

Configuration

createClient(config) accepts:

| Option | Type | Description | | --------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | domain | string | Required. Your Faable Auth tenant domain. The protocol is optional — tenant.auth.faable.link and https://tenant.auth.faable.link are equivalent. | | clientId | string | Required. Application client ID. | | redirectUri | string | Default callback URL. Falls back to window.location.origin. | | scope | string | Space-separated scopes. Defaults to openid profile email. | | storage | SupportedStorage | Custom storage adapter. Defaults to localStorage. | | storageKey | string | Prefix for the storage key. Final key is ${storageKey}-${clientId}. | | cookieOptions | CookieOptions | When set, switches storage to the cookie adapter. | | lock | LockFunc | Custom locking primitive for concurrent refreshes. | | debug | boolean | Enables verbose logging. |

Authentication flows

OAuth / social connection

// Use the default connection configured on the tenant
await auth.signInWithOauthConnection({})

// Or pick a specific provider (by name or connection_id)
await auth.signInWithOauthConnection({
  connection_id: 'conn_01HX…', // preferred when known; falls back to `connection` for legacy tenants
  redirectTo: 'https://app.example.com/callback',
  scopes: 'openid profile email',
  queryParams: { prompt: 'select_account' }
})

In browsers the SDK uses the PKCE flow by default and exchanges the code for a session on the callback page. The first call to createClient automatically processes the URL when the user lands back on the redirect target.

On the redirect success path the returned promise does not resolve — the browser is already navigating away, so a loading state you bind to the await stays on until the page unloads instead of flashing back to idle. Do not re-enable UI after the await on this path.

To control the navigation yourself (e.g. custom timing, or a non-redirecting runtime), pass skipBrowserRedirect: true. The call then resolves with the authorization URL and leaves the navigation to you:

const { data, error } = await auth.signInWithOauthConnection({
  connection: 'google',
  skipBrowserRedirect: true
})
if (error) throw error
window.location.assign(data.url)

Username + password

await auth.signInWithUsernamePassword({
  username: '[email protected]',
  password: '••••••••',
  redirectTo: 'https://app.example.com/callback'
})

Passwordless (magic link or OTP)

// Step 1 — request a code or link
await auth.signInWithPasswordless({
  email: '[email protected]',
  type: 'code' // or "link"
})

// Step 2 — complete the login with the OTP the user received
const { data, error } = await auth.signInWithOtp({
  username: '[email protected]',
  otp: '123456'
})

Password reset

await auth.changePassword({ email: '[email protected]' })

Sign out

By default signOut() (global scope, in a browser) navigates the page to the auth server's /logout to clear the SSO cookie, then returns to returnTo if you pass one. This matters: the SSO cookie lives on the auth domain, so a cross-origin fetch from your app can neither send nor clear it. Without the navigation the SSO session survives and the next signInWith… silently re-logs the previous user, ignoring the requested connection.

await auth.signOut() // clears local + redirects to /logout to clear the SSO cookie
await auth.signOut({ returnTo: 'https://app.example.com/bye' }) // + landing page
await auth.signOut({ scope: 'local' }) // only this device's storage, no redirect
await auth.signOut({ redirect: false }) // legacy: local + best-effort fetch, no nav

returnTo maps to the OIDC post_logout_redirect_uri and must be registered as a logout URL on the client, or the server responds 400.

On the redirect path the returned promise does not resolve (the browser is navigating away) — do not re-enable UI after the await. To drive the navigation yourself, build the URL with getLogoutUrl:

window.location.assign(
  auth.getLogoutUrl({ returnTo: 'https://app.example.com' })
)

Error handling

The client has one error contract, applied uniformly: every asynchronous method resolves with { data, error } and never throws for an expected failure (bad credentials, wrong OTP, missing session, network error…). On success error is null; on failure data is null and error is an AuthError. Always check error before reading data:

const { data, error } = await auth.signInWithOtp({ username, otp })
if (error) {
  showError(error.message)
  return
}
useSession(data.session)

The only thing that throws is createClient itself, and only for a misconfiguration (missing domain / clientId) — a programming error you fix once, not a runtime condition to catch.

This applies to signInWithOauthConnection, signInWithUsernamePassword, signUp, signInWithOtp, signInWithPasswordless, changePassword, changeEmail, signOut, getSession, setSession, refreshSession, initialize and handleRedirectCallback. Their return types (AuthResult<T>, AuthResponse, OAuthResponse) are all variants of the same shape.

Prefer throw-style? Use unwrap

If your code path would rather let errors propagate (a server handler, a try/catch, a wrapper that normalizes everything to throws), wrap the call in unwrap instead of hand-writing if (error) throw error:

import { unwrap } from '@faable/auth-js'

// returns data on success, throws the AuthError on failure
const { session } = unwrap(await auth.signInWithOtp({ username, otp }))

Sessions and state changes

// Get the current session (refreshes if needed)
const {
  data: { session }
} = await auth.getSession()

// Subscribe to auth events
const {
  data: { subscription }
} = auth.onAuthStateChange((event, session) => {
  // event: INITIAL_SESSION | SIGNED_IN | SIGNED_OUT | TOKEN_REFRESHED | PASSWORD_RECOVERY | USER_UPDATED
})

// Stop listening
subscription.unsubscribe()

// Force a refresh
await auth.refreshSession()

Auth events are broadcast across tabs using BroadcastChannel, so a sign-in or sign-out in one tab is reflected in every other tab using the same storageKey.

Storage adapters

Trade-offs

Refresh tokens are sensitive: anyone who reads them can impersonate the user until the token is revoked. The storage you pick decides where they live:

  • localStorage (default) — simple and supports cross-tab sync via BroadcastChannel, but any script running on the same origin can read it. A single XSS lets an attacker exfiltrate the refresh token. Acceptable for low-risk apps and prototypes; not recommended when the surface has third-party scripts, user-generated HTML, or strict compliance requirements.
  • Cookies — required for SSR (server reads them on every request) and the only adapter that lets you scope storage with Secure, SameSite, and Domain. Note that this library writes cookies from JavaScript, so they cannot be marked HttpOnly; an XSS can still read them, but cookies make CSRF and same-site policies enforceable in a way localStorage does not.
  • Custom adapter — use for in-memory storage (tokens lost on reload, safest against XSS), Web Workers, or platform-specific keychains.

If your app is exposed to untrusted content, prefer cookies with Secure: true and SameSite: "Lax" (or "Strict"), and treat XSS prevention (CSP, escaping, framework guarantees) as a hard requirement regardless of which adapter you pick.

localStorage (default)

Used automatically in browsers. No configuration required.

Cookies

Useful for SSR setups where the server must read the session from the request.

import { createClient } from '@faable/auth-js'

export const auth = createClient({
  domain: '<faableauth_domain>',
  clientId: '<client_id>',
  storage: 'cookie'
})

That's it. The adapter sets sensible defaults: Path=/, SameSite=Lax, auto Secure on HTTPS, and a 30-day Max-Age so users stay signed in across browser restarts.

Use cookieOptions only when you need to override something — e.g. share the session across subdomains:

createClient({
  domain: '<faableauth_domain>',
  clientId: '<client_id>',
  storage: 'cookie',
  cookieOptions: { domain: '.example.com' }
})

Custom adapter

Provide any object that implements getItem, setItem, and removeItem (sync or async). Set isServer: true if values may come from an untrusted source such as request cookies.

const memoryStorage = {
  store: new Map<string, string>(),
  getItem: (k: string) => memoryStorage.store.get(k) ?? null,
  setItem: (k: string, v: string) => void memoryStorage.store.set(k, v),
  removeItem: (k: string) => void memoryStorage.store.delete(k)
}

createClient({ domain, clientId, storage: memoryStorage })

Next.js / server-side

Use storage: 'cookie' on the client, then read the session on the server with getSessionFromCookies. It returns the full Session (access_token, refresh_token, expires_at, user) or null, and accepts the cookies() object from next/headers, a NextRequest.cookies object, or a plain { name: value } map.

getSessionFromCookies is async — always await it. In Next.js 15+, cookies() is also async, so await that too:

// app/page.tsx (Next.js 15+)
import { cookies } from 'next/headers'
import { getSessionFromCookies } from '@faable/auth-js'

export default async function Page() {
  const session = await getSessionFromCookies(await cookies(), {
    clientId: '<client_id>'
  })
  if (!session) return <SignIn />
  return <Dashboard user={session.user} />
}

On Next.js 14 and earlier cookies() is synchronous — drop the inner await (await getSessionFromCookies(cookies(), …)).

Gating routes in middleware (Edge)

To keep protected content from ever reaching the browser without a session, gate it in middleware.ts. Pass req.cookies (a NextRequest.cookies object) directly:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { getSessionFromCookies } from '@faable/auth-js'

export async function middleware(req: NextRequest) {
  const session = await getSessionFromCookies(req.cookies, {
    clientId: '<client_id>'
  })
  if (!session) return NextResponse.redirect(new URL('/login', req.url))
  return NextResponse.next()
}

export const config = { matcher: ['/((?!login|_next|favicon.ico).*)'] }

Pass the same clientId you used in createClient. If you also passed a custom storageKey to createClient, mirror it here as { clientId, storageKey } so the helper looks at the same cookie.

Security note. This library writes the session cookie from JavaScript, so it cannot be HttpOnly — an XSS can read the access_token. Treat XSS prevention (CSP, escaping) as a hard requirement. The cookie may also be chunked across faableauth-<clientId>.0, .1, … when large; getSessionFromCookies reassembles the chunks for you, but any code that reads the cookie by hand (another backend, an edge worker) must rejoin them.

returnTo vs redirectTo. Don't embed returnTo inside the redirectTo query (e.g. redirectTo: '/callback?returnTo=/x') — pass returnTo as its own option (signInWith…({ returnTo: '/x' })). The SDK stores it locally next to the PKCE verifier and round-trips it back to you; keep redirectTo a clean URL with no query.

Documentation

For the full guides, API reference, and dashboard setup walkthroughs visit faable.com/docs.

License

See LICENSE.md.