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

@tailor-platform/auth-public-client

v0.5.0

Published

Tailor Platform OAuth2 public client with DPoP+PKCE support

Readme

@tailor-platform/auth-public-client

Browser client library for Tailor Platform authentication using the OAuth 2.0 public client flow with DPoP (Demonstrating Proof of Possession).

Overview

This library provides an authentication solution for browser-based applications integrating with the Tailor Platform. It implements OAuth 2.0 Authorization Code Flow with PKCE and DPoP token binding (RFC 9449) for enhanced security.

Key Features:

  • OAuth 2.0 Authorization Code Flow with PKCE
  • DPoP (Demonstrating Proof of Possession) token binding for enhanced security
  • Automatic token refresh
  • IndexedDB-based secure storage
  • TypeScript support
  • Event-based state management

The library provides a functional approach with the createAuthClient function and has been designed with a modular architecture for maintainability.

Installation

npm install @tailor-platform/auth-public-client

Basic Usage

import { createAuthClient } from "@tailor-platform/auth-public-client";

const authClient = createAuthClient({
  clientId: "your-client-id",
  appUri: "https://your-tailor-app-xxxxxxxx.erp.dev",
  redirectUri: "https://your-app.com/callback", // Optional: defaults to current origin
});

React Integration Example with Suspense

The library is designed to work with React's Suspense for optimal performance and consistency. The AuthState includes an isReady field that indicates whether the initial authentication check has completed.

// useAuth.ts
import { useSyncExternalStore, useCallback } from "react";
import { createAuthClient } from "@tailor-platform/auth-public-client";

// Create auth client instance
const authClient = createAuthClient({
  clientId: "your-client-id",
  appUri: "https://your-tailor-app-xxxxxxxx.erp.dev",
  redirectUri: window.location.origin + "/callback",
});

// Subscribe function for useSyncExternalStore
const subscribe = (callback: () => void) => {
  return authClient.addEventListener((event) => {
    if (event.type === "auth_state_changed") {
      callback();
    }
  });
};

const getSnapshot = () => authClient.getState();

// Suspense-compatible initialization
let initPromise: Promise<void> | null = null;
let initStatus: "pending" | "fulfilled" | "rejected" = "pending";
let initError: Error | null = null;

function getInitPromise(): Promise<void> {
  if (initPromise === null) {
    const params = new URLSearchParams(window.location.search);
    if (params.has("code")) {
      initPromise = authClient.handleCallback().then(() => {
        window.history.replaceState({}, "", window.location.pathname);
      });
    } else {
      initPromise = authClient.checkAuthStatus().then(() => {});
    }

    initPromise
      .then(() => {
        initStatus = "fulfilled";
      })
      .catch((error) => {
        initStatus = "rejected";
        initError = error;
      });
  }
  return initPromise;
}

// React 18+ compatible Suspense - throws Promise while pending
function useSuspenseInit(): void {
  getInitPromise();
  if (initStatus === "pending") throw initPromise;
  if (initStatus === "rejected") throw initError;
}

export function useAuth() {
  // Suspense: throw Promise while initialization is pending
  useSuspenseInit();

  const authState = useSyncExternalStore(subscribe, getSnapshot);

  const login = useCallback(async () => {
    await authClient.login();
  }, []);
  const logout = useCallback(async () => {
    await authClient.logout();
  }, []);

  return { ...authState, login, logout };
}

Note: The isReady field in AuthState indicates whether the initial authentication check has completed. When using Suspense, the loading state is handled automatically by the Suspense boundary.

App Setup with Suspense

// main.tsx
import { StrictMode, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <Suspense fallback={<div>Loading...</div>}>
      <App />
    </Suspense>
  </StrictMode>
);
// App.tsx
import React from 'react';
import { useAuth } from './useAuth';

const App: React.FC = () => {
  const { isAuthenticated, error, login, logout } = useAuth();

  if (error) {
    return <div>Error: {error}</div>;
  }

  if (!isAuthenticated) {
    return (
      <div>
        <h1>Welcome</h1>
        <button onClick={login}>
          Log In
        </button>
      </div>
    );
  }

  return (
    <div>
      <h1>You are authenticated!</h1>
      <button onClick={logout}>
        Log Out
      </button>
    </div>
  );
};

export default App;

Making Authenticated API Requests

Use authClient.fetch() to make authenticated requests. It has the same signature as the standard fetch API and transparently handles DPoP proof generation and token refresh:

const response = await authClient.fetch("https://your-app.erp.dev/query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ query: `query { ... }` }),
});

It can also be passed directly to GraphQL clients that accept a custom fetch option:

// urql
const urqlClient = new Client({
  url: graphqlEndpoint,
  exchanges: [cacheExchange, fetchExchange],
  fetch: authClient.fetch,
});

// Apollo
const link = createHttpLink({
  uri: graphqlEndpoint,
  fetch: authClient.fetch,
});

Using getAuthHeaders() (Advanced)

If you need lower-level control, getAuthHeaders() returns raw DPoP headers. Important: it must be called for every request — do NOT reuse the returned headers.

const headers = await authClient.getAuthHeaders("https://your-app.erp.dev/query", "POST");

const response = await fetch("https://your-app.erp.dev/query", {
  method: "POST",
  headers: {
    ...headers,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ query: `query { ... }` }),
});

Vanilla JavaScript Example

import { createAuthClient } from "@tailor-platform/auth-public-client";

const authClient = createAuthClient({
  clientId: "your-client-id",
  appUri: "https://your-tailor-app-xxxxxxxx.erp.dev",
  redirectUri: window.location.origin + "/callback",
});

// Listen for auth state changes
authClient.addEventListener((event) => {
  if (event.type === "auth_state_changed") {
    const { isAuthenticated, error, isReady } = event.data;

    if (!isReady) {
      document.getElementById("user-info").textContent = "Loading...";
      return;
    }

    if (error) {
      document.getElementById("error-info").textContent = `Error: ${error}`;
      return;
    }

    if (isAuthenticated) {
      document.getElementById("user-info").textContent = "Authenticated";
      document.getElementById("login-btn").style.display = "none";
      document.getElementById("logout-btn").style.display = "block";
    } else {
      document.getElementById("user-info").textContent = "Not signed in";
      document.getElementById("login-btn").style.display = "block";
      document.getElementById("logout-btn").style.display = "none";
    }
  }
});

// Initialize authentication
async function initAuth() {
  const params = new URLSearchParams(window.location.search);
  if (params.has("code")) {
    // Handle OAuth callback
    await authClient.handleCallback();
    window.history.replaceState({}, "", window.location.pathname);
  } else {
    // Check existing auth status
    await authClient.checkAuthStatus();
  }
}

initAuth();

// Login button
document.getElementById("login-btn").addEventListener("click", () => {
  authClient.login();
});

// Logout button
document.getElementById("logout-btn").addEventListener("click", () => {
  authClient.logout();
});

// Make authenticated API request
async function fetchData() {
  const response = await authClient.fetch("https://your-app.erp.dev/query", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      query: `query { ... }`,
    }),
  });

  return response.json();
}

API Reference

createAuthClient

The main function for creating an authentication client.

Function Signature

createAuthClient(config: AuthClientConfig): AuthClient

Configuration

interface AuthClientConfig {
  clientId: string; // OAuth client ID (required)
  appUri: string; // Tailor Platform App URI (required)
  redirectUri?: string; // Callback URL after authentication (optional, defaults to current origin)
}

Methods

login(): Promise<void>

Initiates the OAuth authentication flow by redirecting to the authorization server.

await authClient.login();

logout(): Promise<void>

Logs out the user, clears tokens from storage, and resets authentication state.

await authClient.logout();

getState(): Readonly<AuthState>

Returns the current authentication state (read-only reference). The isReady field indicates whether the initial authentication check has completed.

const { isAuthenticated, error, isReady } = authClient.getState();
// isAuthenticated indicates whether the user is authenticated

checkAuthStatus(): Promise<AuthState>

Checks authentication status by verifying stored tokens and refreshing if needed.

const authState = await authClient.checkAuthStatus();

getAuthUrl(): Promise<string>

Generates an authentication URL without triggering redirect (useful for popup flows).

const authUrl = await authClient.getAuthUrl();
window.open(authUrl, "auth-popup", "width=500,height=600");

handleCallback(): Promise<void>

Handles OAuth callback after redirect. Call this when the user returns from the authorization server.

// In your callback route/page
if (window.location.search.includes("code=")) {
  await authClient.handleCallback();
}

ready(): Promise<void>

Returns a Promise that resolves when the initial authentication check has completed. Useful for Suspense integration.

// With React 19's use() hook
import { use } from "react";
use(authClient.ready());

// Or with await
await authClient.ready();

addEventListener(listener: AuthEventListener): () => void

Adds an event listener for authentication events. Returns an unsubscribe function.

const unsubscribe = authClient.addEventListener((event) => {
  console.log("Auth event:", event.type, event.data);
});

// Later, unsubscribe
unsubscribe();

configure(newConfig: Partial<AuthClientConfig>): void

Updates the client configuration.

authClient.configure({
  redirectUri: "https://your-app.com/new-callback",
});

refreshTokens(): Promise<void>

Manually refreshes the access token using the refresh token.

await authClient.refreshTokens();

fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>

Fetches a protected resource with automatic DPoP authentication. Has the same signature as the standard fetch API.

DPoP proof generation, token refresh, and header injection are handled transparently. Can be passed directly to GraphQL clients (urql, Apollo) as a custom fetch option.

const response = await authClient.fetch("https://your-app.erp.dev/query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ query: `query { ... }` }),
});

Parameters:

  • input: URL string, URL object, or Request object
  • init: Standard RequestInit options (method, headers, body, etc.)

Returns: Promise<Response> — standard fetch Response

Error Handling:

  • Throws Error('No valid access token') if no valid token is available and refresh fails

Notes:

  • Absolute URLs are required — relative URLs (e.g. '/api') are not supported because DPoP proof binding requires a full URI
  • All RequestInit options (signal, credentials, mode, cache, etc.) are preserved and forwarded to native fetch

getAuthHeaders(url: string | URL, method?: string): Promise<AuthHeaders>

Generates Authorization and DPoP headers for protected resource requests.

Important: This method MUST be called for every individual HTTP request. Do NOT cache or reuse the returned headers. Each DPoP proof is unique and single-use.

const headers = await authClient.getAuthHeaders("https://your-app.erp.dev/query", "POST");
// Returns:
// {
//   Authorization: 'DPoP eyJhbGci...',
//   DPoP: 'eyJ0eXAi...'
// }

Parameters:

  • url: The target URL for the request
  • method: HTTP method (default: 'GET')

Returns: Promise<AuthHeaders> containing:

  • Authorization: DPoP token in format DPoP {access_token}
  • DPoP: Signed JWT proof

Internal Behavior:

This method performs the following steps internally:

  1. Token Expiry Check: Checks if the current access token will expire within 60 seconds
  2. Automatic Token Refresh: If the token is expiring soon, automatically refreshes it using the stored refresh token
  3. State Update: If a token refresh occurred, updates the internal state and emits a token_refresh event
  4. DPoP Proof Generation: Generates a fresh DPoP proof JWT bound to:
    • The HTTP method (htm claim)
    • The request URI (htu claim)
    • The access token hash (ath claim)
    • A unique identifier (jti claim)
    • The current timestamp (iat claim)
    • The server-provided nonce (nonce claim, if available)

Error Handling:

  • Throws Error('No valid access token') if no valid token is available and refresh fails
  • If token refresh fails, the method will not return headers (ensure proper error handling in your code)

Types

AuthState

interface AuthState {
  isAuthenticated: boolean; // Whether the user is authenticated
  error: string | null; // Error message (if any)
  isReady: boolean; // Whether initial auth check has completed
}

Note: The isReady field indicates whether the initial authentication check has completed. When using React's Suspense, the loading state is handled automatically by the Suspense boundary.

AuthHeaders

interface AuthHeaders {
  Authorization: string; // "DPoP {access_token}"
  DPoP: string; // DPoP proof JWT
}

AuthEvent

interface AuthEvent {
  type: "login" | "logout" | "token_refresh" | "auth_error" | "auth_state_changed";
  data?: any;
}

Event Types:

  • login: Fired when user successfully logs in
  • logout: Fired when user logs out
  • token_refresh: Fired when tokens are successfully refreshed
  • auth_error: Fired when authentication errors occur
  • auth_state_changed: Fired when authentication state changes

Security Features

OAuth 2.0 with PKCE

The library implements OAuth 2.0 Authorization Code Flow with PKCE (Proof Key for Code Exchange) for secure authorization without requiring a client secret.

DPoP Token Binding (RFC 9449)

DPoP (Demonstrating Proof of Possession) binds access tokens to a cryptographic key pair, preventing token theft and replay attacks.

Each DPoP proof JWT contains:

  • typ: dpop+jwt
  • alg: ES256 (ECDSA P-256)
  • jwk: Public key in JWK format
  • jti: Unique identifier
  • htm: HTTP method
  • htu: Request URI
  • iat: Issued at timestamp
  • ath: Access token hash (SHA-256, base64url encoded)

Note: DPoP nonce handling for token requests is automatically managed by the underlying openid-client library (RFC 9449 Section 8).

State Parameter Validation

Built-in CSRF protection through state parameter validation during authentication callback handling.

IndexedDB Storage

Tokens and DPoP key pairs are stored in IndexedDB for persistence across sessions.

Automatic Token Refresh

Access tokens are automatically refreshed before expiry to ensure uninterrupted API access.

Browser Compatibility

This library requires:

  • Web Crypto API (for DPoP key generation and signing)
  • IndexedDB (for secure storage)
  • ES2020+ support

Supported Browsers:

  • Chrome 80+
  • Firefox 78+
  • Safari 14+
  • Edge 80+

Development

Building the Library

# Install dependencies
npm install

# Build
npm run build

# Run tests
npm test

# Type check
npm run typecheck

Project Structure

auth-public-client/
├── src/
│   ├── auth-client.ts     # Main authentication function (createAuthClient)
│   ├── index.ts           # Entry point and exports
│   ├── types/             # Type definitions
│   │   ├── auth.ts        # Authentication-related types
│   │   ├── config.ts      # Configuration types
│   │   └── index.ts       # Type exports
│   ├── internal/          # Internal implementation modules
│   │   ├── store.ts       # State management
│   │   ├── auth-operations.ts  # Authentication operations
│   │   ├── event-system.ts     # Event handling system
│   │   ├── config-management.ts # Configuration management
│   │   └── dpop-manager.ts     # DPoP proof generation
│   └── utils/             # Utility functions
│       ├── storage.ts     # IndexedDB storage
│       └── oauth.ts       # OAuth/OIDC utilities (openid-client wrapper)
├── tests/                 # Test files
├── dist/                  # Built files (generated)
├── package.json           # Package configuration
├── tsconfig.json          # TypeScript config
└── README.md              # This file

Copyright © 2026 Tailor Inc.