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

avanta

v2.0.3

Published

πŸš… OAuth that does more with less. Prebuilt providers, with high user accessilibity.

Readme

Intro

Avanta is a super-lightweight (sub 100kB) TypeScript-first OAuth library for teams who want prebuilt providers with full type safety and zero framework lock-in.

If you like the convenience of NextAuth or Lucia, but want something that:

  • gives you full control over the OAuth flow (no magic, no sessions, no database)
  • returns scope-aware typed data (request email and guilds? the return type includes exactly those fields)
  • uses only the Fetch API (works in Bun, Node 18+, Deno, Cloudflare Workers)
  • has zero dependencies

Avanta is a good fit.

It gives you:

  • 5 prebuilt providers: Discord, GitHub, Google, Microsoft, Twitch, and more to come
  • Scope-level typing: the data you get back is typed to the exact scopes you requested
  • Token management: TokenStore with built-in refresh, serialization, and expiry tracking
  • Scope-dependent actions: Discord's guilds.join and guilds.members.read are only available when you request those scopes

Table of contents

Quickstart

Install:

bun add avanta

Other package managers:

pnpm add avanta
npm i avanta

1) Create a provider

Every provider takes the same shape: clientId, clientSecret, redirectUri, and scopes. The scopes you pass in determine the return type of getData().

import Avanta from "avanta";

const discord = new Avanta.DiscordProvider({
    clientId: process.env.DISCORD_CLIENT_ID!,
    clientSecret: process.env.DISCORD_CLIENT_SECRET!,
    redirectUri: "http://localhost:3000/auth/discord/callback",
    scopes: ["identify", "email", "guilds"],
});

2) Redirect the user to the OAuth page

Generate the authorization URL and redirect the user to it. You can optionally pass a state parameter for CSRF protection.

const url = discord.getOAuthUrl("random-state-string");
// Redirect the user to `url`

3) Exchange the code for tokens

When the user is redirected back, exchange the authorization code for a TokenStore:

const tokenStore = await discord.getTokenStore(code);

The TokenStore holds the access token, refresh token, and expiry timestamp. You can serialize it to JSON for storage:

const serialized = tokenStore.compress(); // JSON string

And restore it later:

import { TokenStore } from "avanta"; // or from the util path
const restored = TokenStore.extract(serialized);

4) Fetch user data

Call getData() with the token store. The return type is automatically narrowed to the scopes you configured:

const data = await discord.getData(tokenStore);

// Because we requested ["identify", "email", "guilds"]:
data.username; // string     (from "identify")
data.email; // string     (from "email")
data.guilds; // Array<...> (from "guilds")
data.tokenStore; // TokenStore (always present, may be refreshed)

If a scope wasn't requested, the field doesn't exist on the type β€” no runtime checks needed.

Providers

All providers share the same constructor shape and public API. The only differences are the available scopes and the shape of the returned data.

Discord

const discord = new Avanta.DiscordProvider({
    clientId: "...",
    clientSecret: "...",
    redirectUri: "...",
    scopes: ["identify", "email", "guilds", "connections"],
});

Available scopes:

| Scope | Data returned | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | identify | id, username, avatar, discriminator, global_name, banner, accent_color, locale, mfa_enabled, premium_type, public_flags | | email | email, verified | | guilds | guilds (array of partial guild objects) | | connections | connections (array of connection objects) | | guilds.join | Enables actions.guilds.join(...) | | guilds.members.read | Enables actions.guilds.members.read(...) | | gdm.join | Group DM join capability | | role_connections.write | Role connection metadata write | | dm_channels.read | DM channel read access |

GitHub

const github = new Avanta.GitHubProvider({
    clientId: "...",
    clientSecret: "...",
    redirectUri: "...",
    scopes: ["read:user", "user:email", "read:org"],
});

Available scopes:

| Scope | Data returned | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | read:user | login, id, node_id, avatar_url, html_url, name, company, blog, location, bio, twitter_username, public_repos, public_gists, followers, following, created_at, updated_at | | user:email | emails (array with email, primary, verified, visibility) | | read:org | orgs (array with login, id, node_id, avatar_url, description) |

Note: GitHub only issues refresh tokens when token expiration is enabled on the GitHub App. If your app doesn't have token expiration, refreshTokens() will throw.

Google

const google = new Avanta.GoogleProvider({
    clientId: "...",
    clientSecret: "...",
    redirectUri: "...",
    scopes: ["openid", "profile", "email"],
});

Available scopes:

| Scope | Data returned | | --------- | -------------------------------------------------------- | | openid | sub (stable Google account ID) | | profile | name, given_name, family_name, picture, locale | | email | email, email_verified |

Google's OAuth URL automatically requests offline access and consent prompt for refresh token support.

Microsoft

const microsoft = new Avanta.MicrosoftProvider({
    clientId: "...",
    clientSecret: "...",
    redirectUri: "...",
    scopes: ["User.Read", "Organization.Read.All", "offline_access"],
});

Available scopes:

| Scope | Data returned | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | User.Read | id, displayName, givenName, surname, mail, userPrincipalName, jobTitle, officeLocation, businessPhones, mobilePhone, preferredLanguage, avatarUrl | | Organization.Read.All | organization (object with id, displayName, address fields, verifiedDomains, or null for personal accounts) | | offline_access | Enables refresh tokens (no data fields) |

Note: The avatarUrl field requires the access token to fetch the actual image bytes. Personal Microsoft accounts may have mail as null β€” use userPrincipalName as a fallback.

Twitch

const twitch = new Avanta.TwitchProvider({
    clientId: "...",
    clientSecret: "...",
    redirectUri: "...",
    scopes: ["user:read:email"],
});

Twitch always returns base profile data regardless of scopes:

| Field | Description | | ------------------- | --------------------------------------------------------- | | id | Unique Twitch numeric ID (as string) | | login | Lowercase login name | | display_name | Case-preserved display name | | type | Account type ("", "admin", "staff", "global_mod") | | broadcaster_type | "", "affiliate", or "partner" | | description | Channel bio | | profile_image_url | Profile image URL | | offline_image_url | Offline stream image URL | | created_at | ISO 8601 account creation date |

Additional scopes:

| Scope | Data returned | | ----------------- | ------------- | | user:read:email | email |

TokenStore

TokenStore is a small utility class that holds OAuth tokens and manages serialization.

import Avanta from "avanta";

// Created automatically by getTokenStore()
const tokenStore = await provider.getTokenStore(code);

// Properties
tokenStore.access_token; // string
tokenStore.refresh_token; // string
tokenStore.access_token_expires_at; // number (ms since epoch)

// Serialize to JSON string (for database storage, cookies, etc.)
const json = tokenStore.compress();

// Restore from JSON string
const restored = TokenStore.extract(json);

When you call getData(), Avanta automatically refreshes the access token if it has expired. The returned data.tokenStore may be a new instance with fresh tokens β€” always persist the returned token store.

const data = await discord.getData(tokenStore);

// IMPORTANT: persist the (possibly refreshed) token store
await db.user.update({
    where: { id: userId },
    data: { tokens: data.tokenStore.compress() },
});

Common API

Every provider exposes the same methods:

| Method | Description | | ------------------------------- | ---------------------------------------------------------------------------------------- | | getOAuthUrl(state?) | Returns the full authorization URL. Pass an optional state string for CSRF protection. | | getTokens(code) | Exchanges an authorization code for raw token data (provider-specific shape). | | getTokenStore(code) | Exchanges a code for a TokenStore (recommended over getTokens). | | refreshTokens(tokenStore) | Refreshes the access token using the refresh token. Returns raw token data. | | refreshTokenStore(tokenStore) | Refreshes and returns a new TokenStore. | | getData(tokenStore) | Fetches all user data for the configured scopes. Auto-refreshes expired tokens. |

Scope-dependent actions (Discord)

Discord's provider exposes an actions object whose methods are typed based on the scopes you requested.

guilds.join β€” Join a user to a guild

Requires the guilds.join scope and a bot token with CREATE_INSTANT_INVITE permission in the target guild.

const discord = new Avanta.DiscordProvider({
    clientId: "...",
    clientSecret: "...",
    redirectUri: "...",
    scopes: ["identify", "guilds.join"],
});

const result = await discord.actions.guilds.join({
    guildId: "123456789",
    tokenStore,
    botToken: process.env.DISCORD_BOT_TOKEN!,
    options: {
        nick: "New Member",
        roles: ["role-id-1"],
        mute: false,
        deaf: false,
    },
});
// result.data is the guild member object (or null if already a member)
// result.tokenStore is the (possibly refreshed) token store

guilds.members.read β€” Read guild member data

Requires the guilds.members.read scope.

const discord = new Avanta.DiscordProvider({
    clientId: "...",
    clientSecret: "...",
    redirectUri: "...",
    scopes: ["identify", "guilds.members.read"],
});

const result = await discord.actions.guilds.members.read({
    guildId: "123456789",
    tokenStore,
});
// result.data is the guild member object

If you don't request these scopes, discord.actions won't have these methods at the type level β€” TypeScript prevents you from calling them.

Framework examples

Next.js (App Router)

// app/auth/discord/route.ts
import Avanta from "avanta";

const discord = new Avanta.DiscordProvider({
    clientId: process.env.DISCORD_CLIENT_ID!,
    clientSecret: process.env.DISCORD_CLIENT_SECRET!,
    redirectUri: "http://localhost:3000/auth/discord/callback",
    scopes: ["identify", "email"],
});

export async function GET() {
    const url = discord.getOAuthUrl();
    return Response.redirect(url);
}
// app/auth/discord/callback/route.ts
export async function GET(req: Request) {
    const url = new URL(req.url);
    const code = url.searchParams.get("code")!;

    const tokenStore = await discord.getTokenStore(code);
    const data = await discord.getData(tokenStore);

    // data.username, data.email, data.tokenStore
    // Store in your database, set a session cookie, etc.

    return Response.redirect("/dashboard");
}

Express / Bun / any Request-based server

import Avanta from "avanta";

const github = new Avanta.GitHubProvider({
    clientId: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    redirectUri: "http://localhost:3000/auth/github/callback",
    scopes: ["read:user", "user:email"],
});

// Redirect route
app.get("/auth/github", (req, res) => {
    res.redirect(github.getOAuthUrl());
});

// Callback route
app.get("/auth/github/callback", async (req, res) => {
    const code = req.query.code as string;
    const tokenStore = await github.getTokenStore(code);
    const data = await github.getData(tokenStore);

    // data.login, data.emails, data.tokenStore
    res.json({ user: data.login, emails: data.emails });
});

API reference

Exports

The default export is an object containing all providers:

import {
    DiscordProvider,
    GitHubProvider,
    GoogleProvider,
    MicrosoftProvider,
    TwitchProvider,
} from "avanta";

Provider constructor

All providers accept the same options:

{
    clientId: string;
    clientSecret: string;
    redirectUri: string;
    scopes: [...Scope[]]; // const tuple β€” drives return type inference
}

TokenStore

| Property / Method | Type | Description | | -------------------------- | --------------------- | ----------------------------------------------- | | access_token | string | The current access token | | refresh_token | string | The refresh token (empty string if unavailable) | | access_token_expires_at | number | Expiry as ms since epoch | | compress() | string | JSON-serialized token data | | compressed | string (getter) | Alias for compress() | | TokenStore.extract(json) | TokenStore (static) | Deserialize from JSON string |

Contributor docs

License

MIT