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

@authpi/idp

v1.1.0

Published

Official TypeScript SDK for AuthPI authentication

Readme

@authpi/idp

Official TypeScript SDK for AuthPI identity provider.

Requirements: Node.js 18+ or modern browser with Web Crypto API

Stability

@authpi/idp follows semantic versioning. Public exports, constructor options, request and response types, and documented method behavior are stable across 1.x; incompatible changes will ship in a new major version.

Installation

npm install @authpi/idp
# or
pnpm add @authpi/idp

Quick Start

import { IdpClient } from "@authpi/idp";

const idp = new IdpClient({
  issuerUrl: "https://idp.authpi.com/i_xxx",
  clientId: "c_xxx",
  clientSecret: "secret", // omit for public clients (SPAs)
  redirectUri: "https://app.example.com/callback",
  // resources: ["https://mcp.authpi.com/mcp"], // optional RFC 8707 default
});

// 1. Create authorization URL
const auth = await idp.createAuthorizationUrl({
  scopes: ["openid", "profile", "email"],
  // org: "org_xxx", // optional selected-org token restriction
});

// 2. Store the authorization object in session, redirect user to auth.url
session.set("oauth", auth);
redirect(auth.url);

// 3. Handle callback - validates state, exchanges code, and verifies nonce
const agent = await idp.exchangeCallback(callbackUrl, await session.get("oauth"));

// 4. Store tokens for future requests
await session.set("tokens", agent.tokens);

// 5. Check authorization
if (agent.hasAccessIn("org_xxx", "write", "projects")) {
  // User can write to projects in org_xxx
}

Machine-to-Machine Authentication

For server-to-server or background service authentication using the client credentials flow:

const idp = new IdpClient({
  issuerUrl: "https://idp.authpi.com/i_xxx",
  clientId: "agt_machine1",
  clientSecret: "secret",
  // No redirectUri needed for client_credentials
});

const agent = await idp.clientCredentials({
  scopes: ["users:read", "users:write"],
});

// Agent uses token-level scopes (no organizations)
agent.hasAccess("read", "users"); // true
agent.type; // PrincipalType.Agent

Resource Indicators (RFC 8707)

Configure a fixed resource set on the client when every authorization and refresh targets the same protected resource. This is the recommended shape for the AuthPI MCP server:

const idp = new IdpClient({
  issuerUrl: "https://idp.authpi.com/i_xxx",
  clientId: "c_xxx",
  clientSecret: "secret",
  redirectUri: "https://app.example.com/callback",
  resources: ["https://mcp.authpi.com/mcp"],
});

Multiple resources are serialized as repeated resource parameters in the supplied order. A per-operation list overrides the client default:

const auth = await idp.createAuthorizationUrl({
  scopes: ["openid", "offline_access"],
  resources: ["https://api.example.com/orders", "https://api.example.com/payments"],
});

// exchangeCallback repeats auth.resources automatically.
const agent = await idp.exchangeCallback(callbackUrl, auth);

// Select a permitted subset for this refresh.
const narrowed = await idp.refresh(agent, {
  resources: ["https://api.example.com/orders"],
});

Omitting resources on authorization, code exchange, or manual refresh uses the client default; an empty array explicitly omits the parameter. AuthorizationUrl carries an immutable copy of the effective set through the callback, but TokenSet remains unchanged. Automatic refresh from createAgent() therefore omits resource by default and relies on the authorization server's stored refresh-family selection. Pass resources to createAgent() when the issuer policy requires the parameter or you need to select a permitted subset.

Session Management

The SDK automatically refreshes expired tokens when creating an agent from stored tokens:

// Load tokens from your session store
const tokens = await session.get("tokens");

// createAgent() automatically refreshes if tokens are expired
const agent = await idp.createAgent(tokens, {
  // Called when tokens are refreshed - persist the new tokens
  onRefresh: async (newTokens) => {
    await session.set("tokens", newTokens);
  },
  // Called when refresh fails - handle the error
  onRefreshError: async (error) => {
    console.error("Session expired:", error);
    await session.destroy();
    // Redirect to login handled by your framework
  },
});

Configuring Auto-Refresh

const idp = new IdpClient({
  // ...
  autoRefresh: true,        // Default: true
  refreshBufferSeconds: 60, // Refresh 60s before expiry (default)
});

// Or disable per-call
const agent = await idp.createAgent(tokens, { autoRefresh: false });

Token Expiration

// Check expiration
agent.expiresAt;              // Unix timestamp
agent.expiresIn;              // Seconds until expiry (negative if expired)
agent.isExpired();            // true if expires within 30 seconds (default clock skew buffer)
agent.isExpired(60);          // true if expires within 60 seconds
agent.isExpired(0);           // true only if actually expired (no buffer)

Authorization

The SDK includes an optional authorization framework based on scopes. You can use it to make local authorization decisions without additional API calls, or you can ignore it entirely and implement your own authorization logic.

The authorization data comes from the organizations claim in the ID token, which AuthPI populates based on your organization memberships, optional selected-org requests, and client organization restrictions. When you request a selected-org token with org, agent.orgId identifies the selected organization.

Using the Built-in Authorization

// Check access across all organizations
agent.hasAccess("read", "users");

// Check access in a specific organization
agent.hasAccessIn("org_xxx", "write", "projects");
agent.hasAccessIn("org_xxx", "delete", "projects.tasks");

// Role checks
agent.isOwnerOf("org_xxx");   // Has "owner" scope
agent.isAdminOf("org_xxx");   // Has "admin" scope
agent.isMemberOf("org_xxx");  // Has any membership

// Get scopes for an organization
const scopes = agent.getScopesFor("org_xxx");
// ["users:read", "projects:**"]

Rolling Your Own Authorization

If the built-in scope system doesn't fit your needs, you can access the raw data directly:

// Access organizations directly
for (const org of agent.organizations) {
  console.log(org.id);       // "org_xxx"
  console.log(org.title);    // "Admin" or null
  console.log(org.scopes);   // ["users:read", "projects:**"]
  console.log(org.joinedAt); // Unix timestamp
}

// Use agent.id for your own authorization lookups
const permissions = await myPermissionService.getPermissions(agent.id);

How Scopes Work

AuthPI uses a hierarchical scope format: resource:action

Basic format:

resource:action
resource.subresource:action

Examples:

  • users:read — Can read users
  • users:write — Can create/update users
  • projects.tasks:delete — Can delete tasks within projects

Wildcards:

| Pattern | Description | |---------|-------------| | users:* | All actions on users (but not sub-resources) | | users:** | All actions on users AND all sub-resources | | *:read | Read access to all top-level resources | | *:** | Full access to everything (super-admin) |

The difference between * and **:

  • projects:* grants projects:read, projects:write, projects:delete
  • projects:* does NOT grant projects.tasks:read (sub-resource)
  • projects:** grants all of the above PLUS projects.tasks:read, projects.tasks.comments:write, etc.

Scope evaluation example:

const scopes = ["projects:**", "users:read"];

// These all return true:
agent.hasAccessIn("org_xxx", "read", "projects");
agent.hasAccessIn("org_xxx", "write", "projects");
agent.hasAccessIn("org_xxx", "delete", "projects.tasks");
agent.hasAccessIn("org_xxx", "read", "projects.tasks.comments");
agent.hasAccessIn("org_xxx", "read", "users");

// These return false:
agent.hasAccessIn("org_xxx", "write", "users");      // Only has users:read
agent.hasAccessIn("org_xxx", "read", "billing");     // No billing scope

Special Role Scopes

Three scopes have special meaning and dedicated helper methods:

| Scope | Method | Typical Use | |-------|--------|-------------| | owner | isOwnerOf(orgId) | Organization billing, deletion, transfer | | admin | isAdminOf(orgId) | Member management, settings | | member | isMemberOf(orgId) | Basic membership check |

These are checked directly (not via wildcard expansion):

// User has scopes: ["owner", "admin", "projects:**"]
agent.isOwnerOf("org_xxx");  // true - has "owner" scope
agent.isAdminOf("org_xxx");  // true - has "admin" scope
agent.isMemberOf("org_xxx"); // true - has any membership

// Note: "*:**" does NOT grant owner/admin status
// These are explicit role assignments, not permissions

Scope Utilities

For advanced use cases, you can use the scope utilities directly:

import { hasAccess, parseScope } from "@authpi/idp";

// Check if a list of scopes grants access
const scopes = ["users:read", "projects:**"];
hasAccess(scopes, "read", "users");           // true
hasAccess(scopes, "write", "projects.tasks"); // true (** is recursive)
hasAccess(scopes, "delete", "users");         // false

// Parse a scope string into its components
parseScope("users.verifiers:write");
// { action: "write", resource: "users", subResources: ["verifiers"] }

parseScope("projects:**");
// { action: "**", resource: "projects", subResources: [] }

Error Handling

The SDK provides specific error types for different failure modes:

import {
  OAuthError,
  TokenExpiredError,
  RefreshError,
  TokenParseError,
  SubjectMismatchError,
  ConfigurationError,
  InsufficientScopeError,
  SessionExpiredError,
  UserBlockedError,
  AccountLinkingRequiredError,
  InteractionRequiredError,
  LoginRequiredError,
  ConsentRequiredError,
} from "@authpi/idp";

try {
  const agent = await idp.createAgent(tokens);
} catch (error) {
  if (error instanceof TokenExpiredError) {
    // Token expired and no refresh token available
    redirectToLogin();
  } else if (error instanceof RefreshError) {
    // Refresh request failed (e.g., refresh token revoked)
    console.error(`Refresh failed: ${error.errorDescription}`);
    console.error(`Status: ${error.statusCode}`);
    redirectToLogin();
  } else if (error instanceof TokenParseError) {
    // ID token missing or malformed
    console.error("Invalid token data");
  } else if (error instanceof SubjectMismatchError) {
    // Security: refreshed token belongs to different user
    console.error(`Expected ${error.expectedSub}, got ${error.actualSub}`);
  }
}

Error Hierarchy

All OAuth errors extend OAuthError:

class OAuthError extends Error {
  error: string;            // OAuth error code
  errorDescription?: string;
}

class TokenExpiredError extends OAuthError {}
class RefreshError extends OAuthError {
  statusCode?: number;      // HTTP status from token endpoint
}
class TokenParseError extends OAuthError {}
class SubjectMismatchError extends OAuthError {
  expectedSub: string;
  actualSub: string;
}

// AuthPI-specific errors
class InsufficientScopeError extends OAuthError {} // Token lacks required scope
class SessionExpiredError extends OAuthError {}     // Server-side session timed out
class UserBlockedError extends OAuthError {}        // Blocked user attempted auth
class AccountLinkingRequiredError extends OAuthError {} // OAuth identity needs linking

// OIDC authorization endpoint errors
class InteractionRequiredError extends OAuthError {} // Silent auth failed
class LoginRequiredError extends OAuthError {}       // No active session
class ConsentRequiredError extends OAuthError {}     // User hasn't consented

User Info

For full profile data beyond the ID token claims:

const userInfo = await idp.getUserInfo(agent);

userInfo.sub;           // "usr_xxx"
userInfo.email;         // "[email protected]"
userInfo.name;          // "John Doe"
userInfo.picture;       // "https://..."
userInfo.orgId;         // "org_xxx" when the token is restricted to one org
userInfo.organizations; // [{ id, title, scopes, joinedAt }]

Logout

const logoutUrl = idp.createLogoutUrl({
  idTokenHint: agent.tokens.idToken,
  postLogoutRedirectUri: "https://app.example.com",
  state: "logout_state",
});

redirect(logoutUrl);

Token Revocation

// Revoke refresh token (recommended on logout)
await idp.revokeToken(agent.tokens.refreshToken, "refresh_token");

// Revoke access token
await idp.revokeToken(agent.tokens.accessToken, "access_token");

API Reference

IdpClient

new IdpClient(config: IdpClientConfig)

interface IdpClientConfig {
  issuerUrl: string;          // OIDC issuer URL
  clientId: string;           // OAuth client ID
  clientSecret?: string;      // Optional for public clients
  redirectUri?: string;       // Required for authorization code flow, optional for client_credentials
  autoRefresh?: boolean;      // Auto-refresh expired tokens (default: true)
  refreshBufferSeconds?: number; // Refresh buffer (default: 60)
  resources?: readonly string[]; // Default repeated RFC 8707 resource values
}

Methods:

| Method | Description | |--------|-------------| | createAuthorizationUrl(options) | Create OAuth authorization URL with PKCE; accepts optional org and repeated resources | | exchangeCallback(callbackUrlOrParams, auth) | Validate callback state, exchange code, and verify ID-token nonce | | exchangeCode(code, codeVerifier, options?) | Exchange authorization code, optionally repeating transaction resources | | createAgent(tokens, options?) | Create agent from stored tokens; auto-refresh options may override resources | | refresh(agent, options?) | Manually refresh tokens, optionally selecting a resource subset | | getUserInfo(agent) | Fetch full user profile | | createLogoutUrl(options?) | Create OIDC logout URL | | clientCredentials(options?) | Authenticate via client credentials (M2M) | | revokeToken(token, hint?) | Revoke a token |

AuthenticatedAgent

interface AuthenticatedAgent {
  // Identity
  id: string;                 // Subject ID (usr_xxx, tok_xxx, key_xxx, agt_xxx)
  type: PrincipalType;        // "user" | "personal_token" | "api_key" | "agent"
  email?: string;
  emailVerified?: boolean;
  orgId?: string;              // Selected org for org-restricted tokens

  // Tokens (for storage)
  tokens: TokenSet;

  // Organizations (from ID token)
  organizations: Organization[];

  // Expiration
  expiresAt: number;          // Unix timestamp
  expiresIn: number;          // Seconds until expiry
  isExpired(buffer? = 30): boolean;

  // Authorization
  hasAccess(action, resource): boolean;
  hasAccessIn(orgId, action, resource): boolean;
  getScopesFor(orgId): string[];
  isOwnerOf(orgId): boolean;
  isAdminOf(orgId): boolean;
  isMemberOf(orgId): boolean;
}

Framework Examples

Express.js Middleware

import { IdpClient, TokenExpiredError } from "@authpi/idp";

const idp = new IdpClient({ /* ... */ });

async function authMiddleware(req, res, next) {
  const tokens = req.session.tokens;
  if (!tokens) {
    return res.redirect("/login");
  }

  try {
    req.agent = await idp.createAgent(tokens, {
      onRefresh: (newTokens) => {
        req.session.tokens = newTokens;
      },
    });
    next();
  } catch (error) {
    if (error instanceof TokenExpiredError) {
      req.session.destroy();
      return res.redirect("/login");
    }
    next(error);
  }
}

// Usage
app.get("/dashboard", authMiddleware, (req, res) => {
  if (!req.agent.hasAccessIn("org_xxx", "read", "dashboard")) {
    return res.status(403).send("Forbidden");
  }
  res.render("dashboard", { user: req.agent });
});

Next.js Server Action

"use server";

import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { IdpClient } from "@authpi/idp";

const idp = new IdpClient({ /* ... */ });

export async function getAgent() {
  const cookieStore = cookies();
  const tokens = JSON.parse(cookieStore.get("tokens")?.value || "null");

  if (!tokens) {
    redirect("/login");
  }

  try {
    return await idp.createAgent(tokens, {
      onRefresh: async (newTokens) => {
        cookieStore.set("tokens", JSON.stringify(newTokens), {
          httpOnly: true,
          secure: true,
          sameSite: "lax",
        });
      },
      onRefreshError: () => {
        cookieStore.delete("tokens");
        redirect("/login");
      },
    });
  } catch {
    redirect("/login");
  }
}

License

MIT