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

@eetr/eetr-auth-client

v0.4.0

Published

TypeScript client library for eetr-auth OAuth 2.1 / OIDC server

Readme

@eetr/eetr-auth-client

TypeScript client library for the eetr-auth OAuth 2.1 / OIDC server.

It wraps the server's token, introspection, UserInfo, admin, and passkey-management endpoints, plus helpers for OIDC discovery and JWT verification. Everything is fetch-based and ships with full type definitions.

Installation

npm install @eetr/eetr-auth-client
pnpm add @eetr/eetr-auth-client
# or
yarn add @eetr/eetr-auth-client

Requirements: Node.js 18+ (the library relies on the global fetch; decodeJwtPayload also uses Node's Buffer). jose is a runtime dependency used for JWT verification.

The package is ESM-only ("type": "module") and exports both JavaScript and .d.ts types.

Quick start

import {
  fetchOIDCDiscovery,
  exchangeToken,
  validateJwt,
  getUserInfo,
} from "@eetr/eetr-auth-client";

const ISSUER = "https://auth.example.com";

// 1. Discover endpoints
const discovery = await fetchOIDCDiscovery(ISSUER);

// 2. Exchange an authorization code for tokens (PKCE)
const tokens = await exchangeToken(
  {
    grantType: "authorization_code",
    clientId: "my-client",
    code: authorizationCode,
    redirectUri: "https://app.example.com/callback",
    codeVerifier,
  },
  { tokenEndpoint: discovery.token_endpoint }
);

// 3. Verify the access/ID token against the server's JWKS
const payload = await validateJwt(tokens.access_token, discovery.jwks_uri, {
  issuer: ISSUER,
  audience: "my-client",
});

// 4. Fetch the user's profile
const user = await getUserInfo(tokens.access_token, discovery.userinfo_endpoint);

API reference

Discovery

fetchOIDCDiscovery(issuerUrl: string): Promise<OIDCDiscovery>
fetchOAuthMetadata(issuerUrl: string): Promise<OAuthServerMetadata>

Fetch the server's /.well-known/openid-configuration or /.well-known/oauth-authorization-server metadata. Use the returned token_endpoint, jwks_uri, userinfo_endpoint, etc. to configure the rest of the client rather than hard-coding paths.

Authorization URL

buildAuthorizationUrl(authorizationEndpoint: string, params: AuthorizationUrlParams): string

Builds the authorization-request URL for the authorization_code + PKCE flow. code_challenge is required (the server only accepts S256). Pass nonce to request an OIDC nonce that the server binds into the issued id_token, then verify it on the returned token with validateIdToken.

const url = buildAuthorizationUrl(discovery.authorization_endpoint, {
  clientId: "my-client",
  redirectUri: "https://app.example.com/callback",
  codeChallenge,            // base64url SHA-256 of your PKCE verifier
  scope: "openid profile email",
  state,
  nonce,
});

Prefer the OIDCScope constants and the scopes array so a typo can't silently drop openid (which would make /userinfo return 403 insufficient_scope). scope and scopes are merged and de-duplicated, so either or both work:

import { OIDCScope, STANDARD_OIDC_SCOPES } from "@eetr/eetr-auth-client";

const url = buildAuthorizationUrl(discovery.authorization_endpoint, {
  clientId: "my-client",
  redirectUri: "https://app.example.com/callback",
  codeChallenge,
  scopes: [OIDCScope.OpenId, OIDCScope.Profile, OIDCScope.Email], // or STANDARD_OIDC_SCOPES
  state,
  nonce,
});

The client must be granted these scopes by an admin, and openid/profile/email are seeded on the server. Requesting a scope the client wasn't granted fails with invalid_scope.

Token exchange

exchangeToken(params: ExchangeTokenParams, config: ExchangeTokenConfig): Promise<TokenResponse>

Performs an OAuth token request. grantType is one of "authorization_code", "client_credentials", or "refresh_token"; supply the fields relevant to the grant. When the openid scope was granted on an authorization_code exchange, the TokenResponse also includes a signed id_token.

// Client credentials (machine-to-machine)
const tokens = await exchangeToken(
  {
    grantType: "client_credentials",
    clientId: "service-a",
    clientSecret: process.env.CLIENT_SECRET,
    scope: "admin",
  },
  { tokenEndpoint: discovery.token_endpoint }
);

On a non-2xx response it throws an OAuthError carrying the server's error code and error_description.

TokenManager

A small helper that caches an access token and transparently refreshes it (using a 30-second expiry skew) when a refresh token is available.

import { TokenManager } from "@eetr/eetr-auth-client";

const manager = new TokenManager({
  issuerUrl: ISSUER,
  clientId: "my-client",
  clientSecret: process.env.CLIENT_SECRET, // optional for public clients
  tokenEndpoint: discovery.token_endpoint,
});

manager.setTokens(tokens);        // seed from an initial exchange
const accessToken = await manager.getAccessToken(); // refreshes if expired

getAccessToken() throws an OAuthError with code no_token if there is no valid token and no refresh token to fall back on.

JWT verification

validateJwt(token: string, jwksUri: string, options?: ValidateJwtOptions): Promise<JWTPayload>
validateIdToken(token: string, jwksUri: string, options?: ValidateIdTokenOptions): Promise<IDTokenClaims>
decodeJwtPayload(token: string): JWTPayload

validateJwt verifies the signature against the server's JWKS (remote keys are cached per jwksUri) and validates issuer/audience/expiry (clockTolerance defaults to 5 seconds). decodeJwtPayload decodes the payload without verifying the signature — use it only for inspecting claims you have already verified.

const payload = await validateJwt(accessToken, discovery.jwks_uri, {
  issuer: ISSUER,
  audience: "my-client",
  clockTolerance: 10,
});

validateIdToken verifies an OIDC id_token the same way and, when you pass the nonce you sent to the authorization endpoint, additionally checks the token's nonce claim — throwing id_token nonce mismatch on a mismatch. It returns the typed IDTokenClaims (sub, auth_time, nonce, at_hash, plus scope-gated name/preferred_username/picture/email/email_verified).

const claims = await validateIdToken(tokens.id_token!, discovery.jwks_uri, {
  issuer: ISSUER,
  audience: "my-client",
  nonce, // the value passed to buildAuthorizationUrl
});

Token introspection

introspectToken(params: IntrospectTokenParams, config: IntrospectTokenConfig): Promise<TokenValidationResponse>

Asks the server whether a token is active within a given environment. The endpoint is published as token_introspection_endpoint in the OAuth metadata (defaults to ${ISSUER}/api/token/validate).

const metadata = await fetchOAuthMetadata(ISSUER);

const result = await introspectToken(
  { token: accessToken, scopes: ["read"], environmentName: "production" },
  { introspectionEndpoint: metadata.token_introspection_endpoint! }
);
// → { valid, active, client_id, expires_at }

UserInfo

getUserInfo(accessToken: string, userInfoEndpoint: string): Promise<UserInfoResponse>

Returns the OIDC UserInfo claims for the bearer token. The endpoint requires the openid scope; only sub is always present, and the remaining claims are gated by scope (profilename/preferred_username/picture, emailemail/email_verified).

On failure it throws an OAuthError. A token that is valid but lacks openid yields a 403, surfaced as code === "insufficient_scope" (use err.isInsufficientScope) so you can re-authorize with the right scopes rather than discarding the token:

try {
  const info = await getUserInfo(accessToken, discovery.userinfo_endpoint);
} catch (err) {
  if (err instanceof OAuthError && err.isInsufficientScope) {
    // token is fine but missing `openid` — restart the dance requesting it
  }
}

Normalized profile

toUserProfile(userInfo: UserInfoResponse, idTokenClaims?: IDTokenClaims): UserProfile

Merges the UserInfo response (and optionally the decoded id_token claims) into a single camelCased UserProfile (sub, name, preferredUsername, picture, email, emailVerified). UserInfo wins; the id_token fills any gap.

const info = await getUserInfo(tokens.access_token, discovery.userinfo_endpoint);
const profile = toUserProfile(info, decodeJwtPayload(tokens.id_token!));
// → { sub, name?, preferredUsername?, picture?, email?, emailVerified? }

Admin API

User management against the server's admin API. Requires an access token from a client configured as an admin API client on the server.

import {
  getAdminUser,
  createAdminUser,
  updateAdminUser,
  deleteAdminUser,
} from "@eetr/eetr-auth-client";

const config = { baseUrl: ISSUER, accessToken }; // AdminClientConfig

const created = await createAdminUser(
  { username: "alice", password: "•••", email: "[email protected]" },
  config
);

await updateAdminUser("alice", { name: "Alice B." }, config);
const user = await getAdminUser("alice", config); // by username or UUID
await deleteAdminUser(created.id, config);

All admin calls throw OAuthError on non-2xx responses.

Passkey management

List, rename, and remove a user's passkeys. The access token must belong to the user whose passkeys are being managed.

import { listPasskeys, renamePasskey, removePasskey } from "@eetr/eetr-auth-client";

const config = { baseUrl: ISSUER, accessToken }; // UserClientConfig

const passkeys = await listPasskeys(config);
await renamePasskey(passkeys[0].id, "Work laptop", config);
await removePasskey(passkeys[0].id, config);

Only passkey management is available here. Creating or authenticating with a passkey is a WebAuthn ceremony that requires a browser and cannot be driven from a server-side client. removePasskey deletes the server-side record only — it does not remove the credential from the device/authenticator.

Error handling

API helpers throw OAuthError (a subclass of Error) on non-2xx responses, exposing the server's machine-readable code alongside the message:

import { OAuthError } from "@eetr/eetr-auth-client";

try {
  await exchangeToken(params, config);
} catch (err) {
  if (err instanceof OAuthError) {
    console.error(err.code, err.message); // e.g. "invalid_grant"
  }
}

OAuthError also exposes the HTTP status and the server's description (error_description) when available, plus an isInsufficientScope convenience for the /userinfo 403 case (token valid but missing openid).

The discovery helpers throw a plain Error with the HTTP status on failure.

Types

The package exports TypeScript types for every request and response shape, including TokenResponse, UserInfoResponse, UserProfile, IDTokenClaims, OIDCDiscovery, OAuthServerMetadata, AuthClientConfig, JWTPayload, TokenValidationResponse, GrantType, ExchangeTokenParams/Config, AuthorizationUrlParams, IntrospectTokenParams/Config, OIDCScopeValue, ValidateJwtOptions, ValidateIdTokenOptions, AdminUserRecord, AdminClientConfig, CreateUserParams, UpdateUserParams, PasskeySummary, and UserClientConfig. It also exports the OIDCScope / STANDARD_OIDC_SCOPES constants, the resolveScopeParam helper, and the toUserProfile profile normalizer.

License

See the eetr-auth repository.