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

@vexxen/auth

v0.14.0

Published

Cliente de autenticación para Vexxen

Readme

@vexxen/auth

TypeScript client for the Vexxen authentication API. It handles registration, login, token refresh, user info and the Google sign-in flow, with optional React components.

  • Zero storage: the library never stores tokens. You decide where to keep them.
  • ESM only, fully typed.
  • React is an optional peer dependency (only needed for @vexxen/auth/react).

What's new in 0.14.0

getAccess(): new field transferredTo. Users can now transfer a subscription to another Vexxen account from their Vexxen dashboard (the other person accepts it there). After that, the previous owner's access says who has it now, so your app can hand over whatever depended on it. See Product access. No breaking changes.

What's new in 0.13.0

  • Two-step verification (a code sent by email after the password). Users turn it on from their Vexxen profile. The popup (authorizeWithPopup) asks for the code by itself: nothing to change. If you call login() directly, it now throws VexxenTwoFactorRequiredError for those users: finish with completeTwoFactorLogin. See Two-step verification.
  • getAccess(): new status trialing (free trial of a paid plan) and field trialEndsAt. Treat trialing like active.

No breaking changes for apps that sign in with the popup.

What's new in 0.11.0

getAccess(userId) and startFreePlan(userId): from your server, check which plan of your product a user has (paid, free or none) and give them the free plan. See Product access. No breaking changes.

Upgrading to 0.10.0

Users always sign in with Vexxen (email and password). Google is no longer a way to sign in: it is a connected account that apps can use behind the scenes (see Apps that use Google).

  • Breaking: authorizeWithPopup({ provider: "google" }) and AuthorizeProvider are gone. The popup ignores the old provider=google parameter and shows the Vexxen form. SignInWithGoogleButton is again only a styled button: it no longer signs in.
  • New: authorizeWithPopup({ connectGoogle: true }), getGoogleAccessToken(userId) and VexxenGoogleNotConnectedError.
  • New: requestPasswordReset(email) and changePassword(accessToken, request).

Upgrading to 0.8.0

Breaking: register() no longer returns tokens. Users must verify their email (Vexxen sends them a link) before they can sign in. register() now returns { status: "verification_required", email, verificationEmailSent, googleLinkUrl? }, login() throws VexxenEmailNotVerifiedError until the email is verified, and registerWithGoogle's onBeforeRedirect receives that result instead of tokens. New: resendVerificationEmail(email).

Installation

bun add @vexxen/auth
# or
npm install @vexxen/auth

React bindings require React 18 or newer:

bun add react

Quick start

import { VexxenAuth } from "@vexxen/auth";

const auth = new VexxenAuth({
  clientId: "your-client-id",
  apiKey: "your-api-key",
  // Optional: enable Google sign-in
  google: {
    redirectUri: "https://your-app.com/auth/callback",
  },
});

Never expose your API key in client-side code that ships to the browser. Call the library from your server (for example a Next.js Route Handler or Server Action).

API

new VexxenAuth(config)

Creates a client. Throws VexxenAuthConfigError if the configuration is invalid.

apiKey is optional since 0.2.0: it is only needed for getAccess, startFreePlan, register, resendVerificationEmail, requestPasswordReset, changePassword, getGoogleAccessToken, login, refreshToken, userInfo, updateUserInfo, updateAvatar, removeAvatar, getBillingDetails, updateBillingDetails, deleteAccount and logout (calling them without it throws VexxenAuthConfigError). authorizeWithPopup only needs clientId.

auth.register(request): Promise<RegisterResult>

Registers a new user. Registering doesn't sign the user in (since 0.8.0): Vexxen emails them a link to verify their address, and until they open it login fails with VexxenEmailNotVerifiedError. Show a "check your inbox" message.

const result = await auth.register({ /* RegisterRequest */ });
// { status: "verification_required", email: "[email protected]", verificationEmailSent: true, googleLinkUrl?: "…" }

When Google sign-in is enabled, the result also includes a Google link URL the user must be redirected to.

auth.resendVerificationEmail(email): Promise<void>

Sends a new verification link. Vexxen always answers the same way (it never reveals whether the account exists or is already verified) and limits it to one email per minute and five per day.

await auth.resendVerificationEmail("[email protected]");

auth.parseGoogleCallback(params: URLSearchParams)

Parses the parameters Vexxen appends when redirecting back to your app after the Google flow.

const data = auth.parseGoogleCallback(new URLSearchParams(window.location.search));

auth.login(request): Promise<Tokens>

Logs a user in and returns the tokens. If the password is right but the email isn't verified yet, it throws VexxenEmailNotVerifiedError (offer resendVerificationEmail). If the user has two-step verification, it throws VexxenTwoFactorRequiredError (see below).

const tokens = await auth.login({ /* LoginRequest */ });
// tokens.accessToken, tokens.refreshToken

Two-step verification

Users turn it on from their Vexxen profile. From then on, after the password, Vexxen emails them a 6-digit code (valid for 10 minutes). If they can't get the email, they can type one of their recovery codes instead (each works once).

With the popup (authorizeWithPopup) Vexxen asks for the code itself: you get the tokens as always.

With login() you ask for the code:

import { VexxenTwoFactorRequiredError } from "@vexxen/auth";

try {
  return await auth.login({ email, password });
} catch (err) {
  if (!(err instanceof VexxenTwoFactorRequiredError)) throw err;
  // err.challenge: { challenge, email: "j***@example.com", expiresAt }
  const code = await askTheUserForTheCode(err.challenge.email);
  return await auth.completeTwoFactorLogin({ challenge: err.challenge.challenge, code });
}
  • auth.resendTwoFactorCode(challenge) emails a new code (the previous one stops working). One per minute, five per sign-in.
  • Wrong code: VexxenApiError with code INVALID_TWO_FACTOR_CODE (the message says how many attempts are left). Expired code: EXPIRED_TWO_FACTOR_CODE (ask for another). After five wrong codes, or with a challenge that's used, expired or from another app: INVALID_TWO_FACTOR_CHALLENGE, and the user has to enter their password again. Too many requests: TWO_FACTOR_RATE_LIMITED (429).
  • The challenge only works in the app (client) that called login().

Managing it (what the Vexxen profile does; every call needs the user's access token and, except the status, their password):

| Method | What it does | | --- | --- | | getTwoFactorStatus(accessToken) | { enabled, enabledAt, recoveryCodesLeft } | | startTwoFactorSetup(accessToken, password) | Emails a code to turn it on. Returns a TwoFactorChallenge. PASSWORD_NOT_SET if the account has no password. | | enableTwoFactor(accessToken, { challenge, code }) | Turns it on. Returns { recoveryCodes }: show them now, they can't be read again. | | disableTwoFactor(accessToken, password) | Turns it off and deletes the recovery codes. | | regenerateRecoveryCodes(accessToken, password) | New { recoveryCodes }; the old ones stop working. |

Vexxen emails the user every time it's turned on or off, or recovery codes are regenerated.

auth.refreshToken(request): Promise<Tokens>

Exchanges a refresh token for a new set of tokens.

auth.userInfo(accessToken): Promise<VexxenUser>

Returns the profile of the user that owns the access token.

const user = await auth.userInfo(tokens.accessToken);

auth.updateUserInfo(accessToken, request): Promise<VexxenUser>

Lets the user who owns the access token update their own profile. Returns the updated user.

const user = await auth.updateUserInfo(tokens.accessToken, {
  firstName: "Alex",
  phone: "+34 600 000 000", // null or "" removes it
});

Only firstName, lastName and phone can be changed; any other field (for example email) is rejected with a VexxenApiError (400).

auth.updateAvatar(accessToken, image): Promise<VexxenUser>

Sets the profile picture of the user who owns the access token. image is a Blob (or File) in JPEG, PNG or WebP format, up to 1 MB. Returns the updated user, whose avatarUrl points to the new picture.

const user = await auth.updateAvatar(tokens.accessToken, file);
// user.avatarUrl → "https://…/api/avatars/<userId>?v=…"

Crop the image to a square and scale it down before uploading (for example to 256×256): the picture is shown small and every app downloads it. Other formats or larger files are rejected with a VexxenApiError (400, code INVALID_AVATAR). The URL changes with every new picture, so it can be cached forever.

auth.removeAvatar(accessToken): Promise<VexxenUser>

Removes the profile picture. Returns the updated user, with avatarUrl: null.

auth.getBillingDetails(accessToken): Promise<BillingDetails>

Returns the billing details of the user who owns the access token. They are shared by every Vexxen app. If the user has never filled them in, you get their full name as legalName, "ES" as country and null everywhere else.

const billing = await auth.getBillingDetails(tokens.accessToken);
// { legalName, taxId, address: { line1, line2, postalCode, city, state, country } }

The address uses the same shape as Stripe's Customer.address.

auth.updateBillingDetails(accessToken, request): Promise<BillingDetails>

Replaces the user's billing details and returns them as stored. Empty strings are stored as null. country must be an ISO 3166-1 alpha-2 code ("ES", "PT", "MX"…) and defaults to "ES".

await auth.updateBillingDetails(tokens.accessToken, {
  legalName: "Acme SL",
  taxId: "B12345678",
  address: { line1: "Calle Velázquez, 34", postalCode: "28001", city: "Madrid", country: "ES" },
});

Card data never goes through this API: payment methods are handled by Stripe.

auth.requestPasswordReset(email): Promise<void>

"Forgot password?": Vexxen emails the user a link to choose a new password. Accounts without a password (for example, migrated from another app) get a link to create one. Vexxen always answers the same way (it never reveals whether the account exists) and limits it to one email per minute and five per day.

await auth.requestPasswordReset("[email protected]");

The link opens a Vexxen page where the user picks the password. It expires in 1 hour (7 days for creating a first password) and works once. Saving it verifies the email and signs the user out of every app.

auth.changePassword(accessToken, request): Promise<void>

Changes the password of the user who owns the access token. The current password is required, so a stolen access token is not enough. Every other session of the user, in every app, stops working. Pass this session's refreshToken to keep it open.

await auth.changePassword(tokens.accessToken, {
  currentPassword: "old-password",
  newPassword: "new-password", // at least 8 characters
  refreshToken: tokens.refreshToken, // optional: keep this session
});

| VexxenApiError | When | | --- | --- | | status 403, code "INVALID_PASSWORD" | Wrong current password. | | status 409, code "PASSWORD_NOT_SET" | The account has no password yet: use requestPasswordReset to create one. | | status 400 | The new password is too short. |

auth.deleteAccount(accessToken, request): Promise<void>

Deletes the account of the user who owns the access token. The user must confirm with their password, so a stolen access token is not enough. Accounts without a password (Google only) confirm with their email instead.

await auth.deleteAccount(tokens.accessToken, { password });
// accounts without password: { confirmEmail: "[email protected]" }

The account is anonymized: personal data is erased and the email becomes free to register again. Every session of the user, in every app, stops working at once. Delete the tokens you stored.

| VexxenApiError | When | | --- | --- | | status 403, code "INVALID_PASSWORD" | Wrong password. | | status 403, code "INVALID_CONFIRMATION" | confirmEmail does not match. | | status 400, code "CONFIRMATION_REQUIRED" | Missing confirmation. |

auth.logout(request): Promise<void>

Revokes a refresh token, so it can no longer be exchanged for new tokens. Call it when the user signs out. The access token stays valid until it expires (15 minutes), so also delete it from wherever you store it.

await auth.logout({ refreshToken: tokens.refreshToken });

auth.authorizeWithPopup(options?): Promise<Tokens>

Opens the Vexxen sign-in page (<baseUrl>/auth/authorize) in a popup, like "Sign in with Google". The user logs in or creates an account there and the popup sends the tokens back to your app with postMessage. Only clientId is needed, so this is the flow to use in the browser: no API key required.

const auth = new VexxenAuth({ clientId: "your-client-id", baseUrl: "https://vexxen.example.com" });

button.addEventListener("click", async () => {
  const tokens = await auth.authorizeWithPopup({ mode: "login" }); // or "register"
});
  • connectGoogle: true (apps that use Google): after signing in, the popup asks the user to connect Google even if they said "Not now" before.
  • Call it directly from a click handler: browsers block popups opened after an await.
  • Your app's origin (for example http://localhost:3000) must be registered in the client's redirectUris on the Vexxen side; otherwise the popup refuses to continue.
  • If the user closes the popup, the promise rejects with VexxenPopupClosedError.

Product access

If your app is a product of the Vexxen catalog (Vexxen links your client to it), your server can check what each user has of it and decide what they can do. Vexxen is the source of truth for plans and payments: users subscribe, upgrade and cancel in their Vexxen dashboard.

const access = await auth.getAccess(vexxenUserId); // the `id` from userInfo()
// {
//   productId: "planify",
//   access: true,
//   status: "active",            // "active" | "trialing" | "canceling" | "past_due" | "free" | "none"
//   plan: { id: "pro", name: "Pro", paid: true },
//   cycle: "monthly",
//   currentPeriodEnd: "2026-11-08T10:00:00.000Z",
//   cancelAtPeriodEnd: false,
//   trialEndsAt: null,           // end of the free trial, if the plan started with one
//   transferredTo: null,         // they transferred the subscription: the Vexxen user id who has it now
//   accessEndsAt: null,
// }
if (!access.access) {
  // No plan: block the app (or give them the free plan, below)
}

| status | access | When | | --- | --- | --- | | active | yes | Paid plan, up to date. | | trialing | yes | Free trial of a paid plan: treat it like active. The first charge is at trialEndsAt. | | canceling | yes | Canceled: access until accessEndsAt (end of the period), then back to the free plan if there is one. | | past_due | yes | A renewal payment failed: access until accessEndsAt (end of the grace period) unless they pay. | | free | yes | Free plan (or their paid plan ended and the product has a free plan). | | none | no | They don't have the product. |

Transfers. A subscription can be transferred to another Vexxen account (the recipient accepts it in their dashboard). The new owner gets the same plan right away and pays from the end of the period already paid. The previous owner loses the paid plan (free if your product has a free plan, otherwise none) and their access has transferredTo set to the new owner's Vexxen user id: use it to move to them whatever they were paying for, for example a company account.

startFreePlan(userId) gives the user the product's free plan if they don't have the product yet (it never touches a paid plan) and returns their access. Call it, for example, the first time a user signs in to your app.

let access = await auth.getAccess(vexxenUserId);
if (!access.access) access = await auth.startFreePlan(vexxenUserId);

| VexxenApiError | When | | --- | --- | | status 403, code "CLIENT_WITHOUT_PRODUCT" | Your client isn't linked to a product: ask Vexxen to link it. | | status 404, code "USER_NOT_FOUND" | The account doesn't exist or was deleted: sign the user out. | | status 409, code "NO_FREE_PLAN" | startFreePlan: the product has no free plan. |

Both need the apiKey: call them from your server. To let users upgrade, link to <vexxen>/dashboard/catalog?product=<productId>: it opens your product's plans.

Apps that use Google

Users always sign in with Vexxen. If your app uses Google APIs on the user's behalf (for example Google Calendar), Vexxen keeps the user's Google connection and gives your server Google access tokens:

  1. Vexxen enables Google for your client with the Google scopes your app needs and a short purpose, for example "sync your deadlines with Google Calendar".
  2. After signing in, the popup asks the user: "Your app uses your Google account to sync your deadlines with Google Calendar." Connect Google takes them to Google; Not now signs them in without Google, and they are not asked again. Either way your app gets the Vexxen tokens as usual.
  3. From your server, get a Google access token for the user, limited to your app's scopes:
import { VexxenGoogleNotConnectedError } from "@vexxen/auth";

try {
  const google = await auth.getGoogleAccessToken(vexxenUserId); // the `id` from userInfo()
  // { accessToken, expiresAt, scopes } → call Google APIs with `Authorization: Bearer ${google.accessToken}`
} catch (err) {
  if (err instanceof VexxenGoogleNotConnectedError) {
    // The user hasn't connected Google to your app (or revoked it): ask them in the browser
    // with auth.authorizeWithPopup({ connectGoogle: true })
  }
}
  • getGoogleAccessToken needs the apiKey: call it from your server, never from the browser. Each call asks Google for a new token, so reuse it until expiresAt.
  • The token only carries your app's scopes, even if the user's Google account granted more to other Vexxen apps.
  • authorizeWithPopup({ connectGoogle: true }) asks again even if the user said "Not now". If they cancel, they are still signed in, without Google.

React

import { VexxenAuthProvider, useVexxenAuth } from "@vexxen/auth/react";

The @vexxen/auth/react entry point exports VexxenAuthProvider, the useVexxenAuth hook and ready-made buttons (VexxenAuthButton, SignInWithGoogleButton, SignInWithVexxenButton). The components use plain HTML/CSS, with no UI framework dependency.

SignInWithVexxenButton

A "Sign in with Google"-style button with the Vexxen lion logo. Clicking it calls authorizeWithPopup.

import { VexxenPopupClosedError } from "@vexxen/auth";
import { VexxenAuthProvider, SignInWithVexxenButton } from "@vexxen/auth/react";

const config = { clientId: "your-client-id", baseUrl: "https://vexxen.example.com" };

export function Login() {
  return (
    <VexxenAuthProvider config={config}>
      <SignInWithVexxenButton
        mode="login" // "register" opens the sign-up screen
        onSuccess={(tokens) => console.log(tokens)}
        onError={(err) => {
          if (!(err instanceof VexxenPopupClosedError)) console.error(err);
        }}
      />
    </VexxenAuthProvider>
  );
}

Props: mode ("login" | "register", default "login"), label, onSuccess, onError, popupWidth, popupHeight, plus any <button> attribute. Default labels: "Continuar con Vexxen" / "Registrarse con Vexxen".

SignInWithGoogleButton

Only a button styled like Google's (props: label, loading, plus any <button> attribute). It does nothing by itself: users sign in with Vexxen.

useVexxenAuth() also exposes authorizeWithPopup(options) (including connectGoogle), which returns Tokens | null and tracks isLoading / error like login and register.

Error handling

All errors extend a common base so you can handle them selectively:

import {
  VexxenApiError,
  VexxenAuthConfigError,
  VexxenAuthError,
  VexxenEmailNotVerifiedError,
  VexxenGoogleNotConnectedError,
  VexxenInvalidCredentialsError,
  VexxenPopupClosedError,
  VexxenTwoFactorRequiredError,
} from "@vexxen/auth";

try {
  await auth.login({ /* ... */ });
} catch (err) {
  // VexxenApiError has: message (the API's message), status, code (e.g. "EMAIL_TAKEN") and body.
  if (err instanceof VexxenInvalidCredentialsError) {
    // wrong email or password
  } else if (err instanceof VexxenTwoFactorRequiredError) {
    // ask for the emailed code: auth.completeTwoFactorLogin({ challenge: err.challenge.challenge, code })
  } else if (err instanceof VexxenApiError) {
    // the API returned an error
  } else {
    throw err;
  }
}

Exported types

AccessStatus, AuthConfig, AuthorizeMode, AuthorizePopupOptions, ChangePasswordRequest, CompleteTwoFactorLoginRequest, EnableTwoFactorRequest, RecoveryCodes, TwoFactorChallenge, TwoFactorStatus, GoogleAccessToken, ProductAccess, BillingAddress, BillingDetails, DeleteAccountRequest, UpdateBillingDetailsRequest, GoogleAuthOptions, LoginRequest, LogoutRequest, RefreshTokenRequest, UpdateUserInfoRequest, RegisterRequest, RegisterResult, Tokens, VexxenUser, AuthResult.

Development

bun install
bun run typecheck     # tsc --noEmit
bun run build         # tsup -> dist/

Manual test scripts (they hit the real API, so set up your environment first):

bun run try:register
bun run try:login
bun run try:refresh-userinfo

Releasing a new version

This package follows Semantic Versioning:

| Change | Command | Example | | ------------------------------ | ------------------- | --------------- | | Bug fix, no API change | npm version patch | 0.1.0 -> 0.1.1 | | New feature, backwards compatible | npm version minor | 0.1.1 -> 0.2.0 | | Breaking change | npm version major | 0.2.0 -> 1.0.0 |

Release checklist:

git status                  # working tree must be clean
npm version patch           # bumps package.json, commits and tags
npm pack --dry-run          # only dist/ and package.json should appear
npm publish                 # prepublishOnly runs typecheck + build
git push --follow-tags

Publishing requires an npm account with two-factor authentication enabled for "Authorization and publishing", or a granular access token with 2FA bypass. A published version cannot be overwritten: if you make a mistake, publish a new version and, if needed, run npm deprecate @vexxen/auth@<version> "reason".

Pre-release versions can be published under a separate tag so they are not installed by default:

npm version prerelease --preid=beta   # 0.2.0-beta.0
npm publish --tag beta
bun add @vexxen/auth@beta

License

MIT