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

@fanvue/builder-sdk

v0.3.0

Published

OAuth authentication library for the Fanvue API

Readme

@fanvue/builder-sdk

Authentication for Fanvue apps. Every Fanvue app is either embedded or off-platform (which should I build?) — this SDK covers both:

| Building an... | Import | What you get | Docs | |---|---|---|---| | Embedded App (runs inside Fanvue, in an iframe) | @fanvue/builder-sdk/nextjs/embedded-app + @fanvue/builder-sdk/react | Session-token exchange handler, useEmbeddedAuth hook, Bearer sessions with auto refresh | Overview · Integration guide | | Off-Platform App ("Login with Fanvue" on your own domain) | @fanvue/builder-sdk/nextjs/off-platform | Full-page redirect flow, httpOnly cookie sessions, auto token refresh | Auth overview · Implementation guide |

Building something else (Node, Deno, a custom server)? The core @fanvue/builder-sdk entrypoint exposes the low-level OAuth primitives both flows are built on.

  • Drop-in route handlers for Next.js -- a couple of files and you're done
  • Handles token exchange, refresh, and session management so you don't have to
  • Fully typed with TypeScript

[!IMPORTANT] Renamed from @fanvue/auth. This package was previously published as @fanvue/auth (≤ 0.2.3). The API is unchanged — to migrate, swap the dependency and update import paths:

- "@fanvue/auth": "^0.2.3"
+ "@fanvue/builder-sdk": "^0.3.0"
- import { useEmbeddedAuth } from "@fanvue/auth/react";
+ import { useEmbeddedAuth } from "@fanvue/builder-sdk/react";

@fanvue/auth is deprecated on npm and will receive no further releases.

Installation

npm install @fanvue/builder-sdk
# or
pnpm add @fanvue/builder-sdk
# or
yarn add @fanvue/builder-sdk

Peer dependencies (next, react) are optional -- only install what your project uses.

Quick Start

Embedded App (your app inside Fanvue)

When Fanvue opens your app in an iframe, it appends a short-lived session token to your embed URL (?token=...). Exchange it server-side for real OAuth tokens -- the SDK handles the whole delegated flow (PKCE, state, code exchange).

1. Add the session-exchange route

// app/api/fanvue/session/route.ts
import { createConfig, createSessionExchangeHandler } from "@fanvue/builder-sdk/nextjs/embedded-app";

export const { POST } = createSessionExchangeHandler(createConfig());

2. Authenticate in your embedded page

// app/embedded/page.tsx
"use client";
import { AuthProvider, useAuth, useEmbeddedAuth } from "@fanvue/builder-sdk/react";

function Embedded() {
  const { status, error, theme } = useEmbeddedAuth();
  const { authFetch } = useAuth();

  // `theme` is the creator's active colour scheme ("light" | "dark"), read
  // from the iframe URL -- use it to match Fanvue. It's `null` when the app is
  // opened outside Fanvue, so fall back to a sensible default.
  return (
    <div data-theme={theme ?? "light"}>
      {status === "exchanging" && <p>Connecting to Fanvue…</p>}
      {status === "error" && <p>Auth failed: {error}</p>}
      <button onClick={() => authFetch("/api/me")}>Load my profile</button>
    </div>
  );
}

export default function Page() {
  return (
    <AuthProvider>
      <Embedded />
    </AuthProvider>
  );
}

3. Call the Fanvue API from your own routes

// app/api/me/route.ts
import { NextResponse } from "next/server";
import { HEADER_UPDATED_SESSION } from "@fanvue/builder-sdk";
import { createConfig, getAuthenticatedClient } from "@fanvue/builder-sdk/nextjs/embedded-app";

const config = createConfig();

export async function GET() {
  const auth = await getAuthenticatedClient({ sessionSecret: config.sessionSecret, config });
  if (!auth) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

  const user = await auth.client.getCurrentUser();
  const res = NextResponse.json(user.isOk() ? user.value : { error: "api_failed" });
  // When the access token was refreshed, hand the new session JWT back to the
  // client -- `authFetch` stores it automatically.
  if (auth.refreshedJwt) res.headers.set(HEADER_UPDATED_SESSION, auth.refreshedJwt);
  return res;
}

Hosting requirements for embedded apps:

  • Serve over HTTPS with a browser-trusted certificate.
  • Send Content-Security-Policy: frame-ancestors https://www.fanvue.com so Fanvue can frame your page.
  • The session token lives ~60 seconds -- exchange it promptly (the useEmbeddedAuth hook does this on mount).
  • To act on the creator's behalf after the iframe closes, persist the refresh token from the onTokens hook:
export const { POST } = createSessionExchangeHandler(createConfig(), {
  onTokens: async ({ tokens, user }) => {
    await db.saveRefreshToken(user.uuid, tokens.refresh_token);
  },
});

See the embedded apps integration guide for the full walkthrough, including app registration in the Builder.

Off-Platform App ("Login with Fanvue")

The fastest path: create a config file and three route handlers.

1. Configure

// lib/auth.ts
import { createConfig } from "@fanvue/builder-sdk/nextjs/off-platform";

export const authConfig = {
  ...createConfig(), // reads OAUTH_* and SESSION_* env vars
  afterLoginPath: "/dashboard",
  afterLogoutPath: "/",
};

2. Add route handlers

// app/api/oauth/login/route.ts
import { createLoginHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
import { authConfig } from "@/lib/auth";

export const { GET } = createLoginHandler(authConfig);
// app/api/oauth/callback/route.ts
import { createCallbackHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
import { authConfig } from "@/lib/auth";

export const { GET, POST } = createCallbackHandler(authConfig);
// app/api/oauth/logout/route.ts
import { createLogoutHandler } from "@fanvue/builder-sdk/nextjs/off-platform";
import { authConfig } from "@/lib/auth";

export const { POST } = createLogoutHandler(authConfig);

3. Use the session

import { getSession, getAuthenticatedClient } from "@fanvue/builder-sdk/nextjs/off-platform";
import { authConfig } from "@/lib/auth";

// Read the session in any server component or route handler
const session = await getSession(authConfig.sessionSecret, authConfig.sessionCookieName);

// Or get an API client that automatically refreshes expired tokens
const client = await getAuthenticatedClient({
  sessionSecret: authConfig.sessionSecret,
  sessionCookieName: authConfig.sessionCookieName,
  config: authConfig,
});

if (client) {
  const user = await client.getCurrentUser();
  if (user.isOk()) console.log(user.value);
}

See the authentication docs for scopes, rate limits, and the underlying OAuth flow.

Core (advanced)

Use the core entrypoint when you need full control -- CLI tools, custom servers, or non-Next.js frameworks.

import {
  createAuthorizationUrl,
  exchangeCodeForToken,
  refreshAccessToken,
  createSessionJwt,
  verifySessionJwt,
  createFanvueClient,
} from "@fanvue/builder-sdk";

const config = {
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  redirectUri: "http://localhost:3000/callback",
  issuerUrl: null,
  apiBaseUrl: null,
  scopes: null,
  responseMode: null,
  prompt: null,
};

// 1. Build the authorization URL (PKCE is handled automatically)
const { url, codeVerifier, state } = await createAuthorizationUrl(config);
// Redirect the user to `url`...

// 2. Exchange the authorization code for tokens
const tokenResult = await exchangeCodeForToken(config, {
  code: "code-from-callback",
  codeVerifier,
  redirectUri: null,
});
if (tokenResult.isErr()) throw new Error(tokenResult.error.message);
const tokens = tokenResult.value;

// 3. Make authenticated API calls
const client = createFanvueClient(tokens.access_token, null);
const userResult = await client.getCurrentUser();
if (userResult.isOk()) console.log(userResult.value);

// 4. Refresh an expired access token
const refreshResult = await refreshAccessToken(config, tokens.refresh_token);

For the embedded flow, the equivalent core primitive is exchangeSessionToken(config, sessionToken), which runs PKCE generation, the delegated authorize-on-behalf request, and the code exchange in one call.

import { createSessionJwt, verifySessionJwt } from "@fanvue/builder-sdk";

// Create a signed session JWT (HS256, default 30-day expiry)
const jwt = await createSessionJwt("your-session-secret", {
  accessToken: tokens.access_token,
  refreshToken: tokens.refresh_token,
  expiresAt: Date.now() + tokens.expires_in * 1000,
  tokenType: tokens.token_type,
  scope: tokens.scope,
  idToken: tokens.id_token,
  userUuid: "...",
  handle: "...",
  displayName: "...",
  isCreator: false,
  avatarUrl: null,
});

// Verify a session JWT
const session = await verifySessionJwt("your-session-secret", jwt);

Environment Variables

Add these to your .env.local (Next.js) or equivalent:

# Required
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
OAUTH_REDIRECT_URI=http://localhost:3000/api/oauth/callback
SESSION_SECRET=at-least-32-characters-long-random-string

# Optional (shown with defaults)
OAUTH_ISSUER_BASE_URL=https://auth.fanvue.com
API_BASE_URL=https://api.fanvue.com
FANVUE_PLATFORM_URL=https://www.fanvue.com   # embedded apps only
OAUTH_SCOPES="openid offline_access offline"
SESSION_COOKIE_NAME=fanvue_session
# OAUTH_RESPONSE_MODE=        # query, form_post, etc.
# OAUTH_PROMPT=                # login, consent, etc.

For the Fanvue dev environment use https://auth.dev.fanvue.com, https://api.dev.fanvue.com, and https://dev.fanvue.com respectively.

The createConfig() helper reads these automatically. You can also override any value programmatically:

const config = createConfig({
  clientId: "explicit-id",         // overrides OAUTH_CLIENT_ID
  sessionCookieName: "my_session", // overrides SESSION_COOKIE_NAME
});

Error Handling

All async operations return Result<T, E> types from neverthrow instead of throwing exceptions. This gives you type-safe, explicit error handling:

const result = await client.getCurrentUser();

if (result.isOk()) {
  console.log(result.value); // FanvueUser
} else {
  console.error(result.error); // ApiError
}

// Or use neverthrow's functional API
result
  .map((user) => console.log(user.displayName))
  .mapErr((err) => console.error(err.message));

Error types: OAuthError (token exchange/refresh), EmbeddedAuthError (delegated authorize-on-behalf), ApiError (API requests), SessionVerifyError (JWT verification).

API Reference

Core (@fanvue/builder-sdk)

| Export | Description | |---|---| | createAuthorizationUrl(config, opts?) | Build a PKCE authorization URL. Returns { url, codeVerifier, state }. | | exchangeCodeForToken(config, opts) | Exchange an authorization code for tokens. Returns Result<TokenResponse, OAuthError>. | | refreshAccessToken(config, refreshToken) | Refresh an expired access token. Returns Result<TokenResponse, OAuthError>. | | exchangeSessionToken(config, sessionToken) | Exchange an embedded session token for tokens (full delegated flow). Returns Result<TokenResponse, EmbeddedAuthError \| OAuthError>. | | requestAuthorizationCodeOnBehalf(config, sessionToken, opts) | Low-level: request a delegated authorization code from the platform. | | getSessionTokenFromUrl(url) | Extract the ?token= session token from a URL. | | getThemeFromUrl(url) | Extract the ?theme= colour scheme from a URL. Returns 'light' \| 'dark' \| null. | | createSessionJwt(secret, payload, expiresIn?) | Create a signed HS256 session JWT. Default expiry: 30 days. | | verifySessionJwt(secret, token) | Verify and decode a session JWT. Returns Result<SessionPayload, SessionVerifyError>. | | createFanvueClient(accessToken, apiBaseUrl?) | Create an authenticated API client. | | API_VERSION | The API version header value (currently 2025-06-26). | | HEADER_UPDATED_SESSION | Response header (X-Updated-Session) carrying a refreshed session JWT. |

Types

| Type | Description | |---|---| | OAuthConfig | Client ID, secret, redirect URI, and optional overrides | | EmbeddedAuthConfig | OAuthConfig plus the Fanvue platform base URL | | FanvueTheme | The creator's active colour scheme: 'light' \| 'dark' | | TokenResponse | Access token, refresh token, expiry, scopes | | SessionPayload | JWT claims: tokens, user info, expiry timestamp | | FanvueUser | User profile (uuid, email, handle, displayName, isCreator, avatarUrl, etc.) | | OAuthError | Error from token exchange or refresh | | EmbeddedAuthError | Error from the delegated authorize-on-behalf flow | | ApiError | Error from API requests | | SessionVerifyError | Error from session JWT verification |

Next.js Embedded App (@fanvue/builder-sdk/nextjs/embedded-app)

| Export | Description | |---|---| | createConfig(opts?) | Build an EmbeddedAppConfig from env vars and/or explicit options (adds platformUrl) | | createSessionExchangeHandler(config, hooks?) | { POST } -- exchanges the embedded session token for tokens, responds with a session JWT | | getSession(secret) | Read and verify the session from the Authorization: Bearer header | | getAuthenticatedClient(opts) | Get a FanvueClient from the Bearer session, with automatic token refresh | | getSessionTokenFromUrl(url) | Extract the ?token= session token from a URL |

Next.js Off-Platform App (@fanvue/builder-sdk/nextjs/off-platform)

| Export | Description | |---|---| | createConfig(opts?) | Build a ResolvedConfig from env vars and/or explicit options | | createLoginHandler(opts) | { GET } -- redirects to the OAuth provider | | createCallbackHandler(opts) | { GET, POST } -- completes the code exchange, sets the session cookie | | createLogoutHandler(opts) | { POST } -- clears the session cookie | | getSession(secret, cookieName?) | Read and verify the session from cookies | | getAuthenticatedClient(opts) | Get a FanvueClient with automatic token refresh |

React (@fanvue/builder-sdk/react)

| Export | Description | |---|---| | AuthProvider | Context provider. Manages JWT storage in sessionStorage. | | useAuth() | Returns { jwt, isAuthenticated, setJwt, clearJwt, authFetch } | | useEmbeddedAuth(opts?) | Returns { status, error, theme }. Exchanges the embedded session token on mount; theme is the creator's colour scheme ('light' \| 'dark' \| null). |

Examples

Complete working examples:

| Example | App type | Auth flow | |---|---|---| | examples/nextjs-embedded-app | Embedded | Session-token exchange (delegated authorize-on-behalf) | | examples/nextjs-off-platform-app | Off-platform | "Login with Fanvue" full-page redirect |

Contributing

pnpm install        # Install dependencies
pnpm build          # Build the library
pnpm dev            # Watch mode
pnpm test           # Run tests
pnpm typecheck      # Type-check
pnpm lint           # Lint
pnpm check          # Run all checks (format + lint + typecheck + test)

License

MIT