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

openauthster-shared

v1.0.7

Published

Shared TypeScript types, database schemas, provider definitions, and client SDK used across the OpenAuthster stack.

Readme

OpenAuthster Shared Types & Client

Shared TypeScript types, database schemas, provider definitions, and client SDK used across the OpenAuthster stack.

What's Inside

  • TypeScript types - Shared domain types and endpoint contracts
  • Database schemas - Drizzle ORM schemas and helpers for the issuer's D1 database
  • Low-level client (openauthster-shared/client) - Thin wrapper around @kagii/openauth that injects client_id, optional copy_id, and the cookie expected by the issuer
  • OpenAuthsterClient (openauthster-shared/client/user) - High-level client class for login, callback handling, token storage, refresh, session management, admin helpers, MFA, and passkey flows
  • CLI SDK (openauthster-shared/client/cli) - Public loopback PKCE client with the same access-token and refresh-token flow
  • Webhook helpers (openauthster-shared/webhook) - Typed webhook payload verification helpers and webhook event definitions

Installation

Local Development

This package is used directly inside the OpenAuthster workspace.

# Clone the OpenAuthster workspace
git clone https://github.com/shpaw415/openauthster-workspace
cd openauthster-workspace

# The shared package is then available to the other workspace projects

Package Managers

The published package name is openauthster-shared.

The repository version is currently v1.0.0. If the npm registry has not caught up yet, consume this package from the workspace sources until the published package is updated.

# npm
npm install openauthster-shared

# Bun
bun add openauthster-shared

# Yarn
yarn add openauthster-shared

OpenAuthsterClient

OpenAuthsterClient is the recommended high-level client for applications. It handles login redirects, token exchange, automatic token refresh, public and private sessions, authenticated fetch calls, and several admin-oriented helpers.

Import it from openauthster-shared/client/user:

import { createOpenAuthsterClient } from "openauthster-shared/client/user";

Creating a Client

const client = createOpenAuthsterClient({
  clientID: "my_project_01",
  issuerURI: "https://auth.yourdomain.com",
  redirectURI: "https://myapp.com/",
  secret: process.env.AUTH_SECRET,
});

| Option | Type | Required | Description | | ------ | ---- | -------- | ----------- | | clientID | string | Yes | Project identifier from the WebUI. | | issuerURI | string | Yes | Base URL of the OpenAuthster issuer. Must start with http or https. | | redirectURI | string | Yes | URL the issuer redirects back to after login. | | secret | string | No | Server-side only. Used for private-session and admin requests that require signed headers. | | token | string | null | No | Pre-existing access token for server-side or restored-client scenarios. | | refreshToken | string | null | No | Pre-existing refresh token. | | subject | SubjectSchema | No | Custom subject schema used when verifying tokens. | | authFlowCallbacks | Partial<{ onQRAuthFlowStart; onLoginRequired }> | No | Optional callbacks for QR auth gating and refresh-failure handling. | | onError | (err: ErrorList) => void | No | Optional error callback used by client helpers such as TOTP flows. | | cache_provider | { get; set; delete } | No | Optional async cache for token-derived user metadata. |

Initialization

Call init() once in the browser after creating the client. On the configured redirectURI page it checks for OAuth callback parameters and exchanges tokens when needed. On any other URL a code search param is ignored so application-specific codes are not consumed. It then restores persisted tokens from localStorage and notifies registered listeners.

await client.init();

init() also fires every listener registered with addInitializationListener, so the UI can re-render when auth state changes.

Login and Logout

// Redirect the user to the issuer login page
await client.login();

// Or get the URL without navigating yet
const authURL = await client.login({ autoNavigate: false });

// Clear tokens, session data, and auth state
await client.logout();

login() stores the PKCE challenge in localStorage. When the user returns to redirectURI (for example https://example.com/auth/callback?code=<exchange_code>), init() detects the callback automatically. A code query param on any other route is left alone. You can also call callback() yourself if you want to handle that step manually.

Manual Callback Handling

The init() method automatically handles OAuth callbacks on redirectURI, but you can manually trigger the callback exchange if needed. callback() no-ops unless the current origin and pathname match redirectURI:

await client.callback();

On success, the client exchanges the authorization code for tokens, stores them locally, and removes code and state from the URL.

Checking Auth State

client.isAuthenticated; // boolean
client.isLoaded; // boolean
client.expiresAt; // Date | null

client.userMeta;
// { id: string | null, identifier: string | null, role: string | null, data: Record<string, unknown> | null }

client.userInfo;
// Provider-backed info loaded from /session responses.
// Includes at least { provider, role } and may include provider-specific fields and email.

client.error;
// { error: string, error_description: string | null } | null

Lightweight Metadata

If you only need token-derived user metadata without loading a session blob, use getMetaData():

const meta = await client.getMetaData();
// { id, identifier, role, data } | null

Public & Private Sessions

OpenAuthster stores two session buckets per user:

  • Public - Readable from browser or server contexts.
  • Private - Intended for server-side use and requires secret on requests.

Reading a Session

const result = await client.getUserSession("public");

if (!(result instanceof Error)) {
  console.log(result.public);
  console.log(client.user_id);
  console.log(client.userInfo?.google?.picture); // with provider google
}

Updating a Session

updateUserSession() uses PATCH-style semantics. Only the provided keys are sent.

await client.updateUserSession("public", {
  displayName: "Alice",
  theme: "dark",
});

// For private sessions (server-side, requires secret):
await client.updateUserSession("private", {
  api_key: "user-api-key",
});

Clearing a Session

// Clear public session data
await client.clearPublicSession();

// Clear private session data (server-side, requires secret)
await client.clearPrivateSession();

Authenticated Fetch

Use client.fetch() for authenticated calls that should send the current bearer token.

  • Adds Authorization: Bearer <token> when a token is present.
  • Adds X-Client-Timestamp and X-Client-Signature when secret is configured.
  • Does not append client_id automatically.

If you need the client to append client_id, use client.fetchWithOptions().

const response = await client.fetch("/api/v1/profile");
const data = await response.json();

Token Management

Getting the Current Token

const token = client.getToken();
// Returns the current access token or retrieves it from localStorage

Setting Token to Cookie

client.setTokenToCookie();
// access_token=<token>; path=/; secure; samesite=lax;

Token Verification

Verify a token's authenticity using the client's subject schema:

// Verify the current client token
const isValid = await client.verify();
console.log(isValid); // true or false

// Verify a specific token
const isTokenValid = await client.verify("eyJhbGciOiJIUzI1NiIs...");

verify() returns true or false. If no token is available and no explicit token is provided, it rejects.

User Management (Admin Features)

These helpers are intended for server-side admin operations and typically require secret to be configured.

Get User by ID

const user = await client.getUserById("user_12345");

if (user instanceof Error) {
  console.error("Failed to fetch user:", user.message);
} else {
  console.log(user.data.users[0]);
}

Get Many Users by ID

const users = await client.getManyUserById(["user_1", "user_2"]);

if (users instanceof Error) {
  console.error("Failed to fetch users:", users.message);
} else {
  console.log(users.data.users);
}

Get Users List

const users = await client.getUsers({
  page: 1,
  limit: 10,
});

if (users instanceof Error) {
  console.error("Failed to fetch users:", users.message);
} else {
  console.log(users.data.users);
  console.log(users.data.total);
}

Update User by ID

Only the provided fields are updated. Omitted fields are left unchanged.

const result = await client.updateUserById("user_12345", {
  session_public: { theme: "dark" },
  session_private: { role: "admin" },
});

if (result instanceof Error) {
  console.error("Failed to update user:", result.message);
} else {
  console.log("User updated:", result);
}

Update User Role by ID

const result = await client.setUserRoleById("user_12345", "admin");

if (result instanceof Error) {
  console.error("Failed to update user role:", result.message);
} else {
  console.log("Updated role:", result);
}

Delete User by ID

const result = await client.deleteUserById("user_12345");

if (!result.success) {
  console.error("Failed to delete user:", result.error);
}

Delete the Current User

const result = await client.deleteCurrentUser();

if (!result.success) {
  console.error("Failed to delete current user:", result.error);
}

On success, deleteCurrentUser() also clears the local authentication state.

Auto Token Refresh

The client stores the issuer-provided token expiry time, schedules a refresh for that expiry point, and also attempts a refresh if an authenticated request detects that the token is already expired.

Refresh attempts retry up to three times with backoff. If they still fail, the client either calls authFlowCallbacks.onLoginRequired or logs the user out.

Listening for State Changes

Register a callback that fires whenever init() completes or triggerUpdate() is called. The callback receives the client instance and an optional error object:

client.addInitializationListener("my-key", (client, error) => {
  if (error) {
    console.error("Auth error:", error.error, error.error_description);
    return;
  }
  console.log("Auth state changed:", client.isAuthenticated);
});

// Manually trigger all listeners (e.g. after updating session data):
await client.triggerUpdate();
client.removeInitializationListener("my-key");

The error parameter is populated when the OAuth flow returns an error (e.g., user denied access, invalid request).

Updating Runtime Options

client.updateOptions({ copyID: "fr-fr" });
client.updateOptions({ secret: process.env.AUTH_SECRET });

Updating copyID recreates the internal OpenAuth client so future login flows use the new template.

MFA and Passkey Helpers

The high-level client also exposes helper objects for TOTP and passkey flows.

const setup = await client.mfa.totpClient.setupTotp();
const elevated = await client.mfa.totpClient.getElevatedToken("123456");

await client.passkey.login();
await client.passkey.register({ userDisplayName: "Alice" });

Useful helper methods include:

  • client.mfa.totpClient.setupTotp()
  • client.mfa.totpClient.confirmSetup({ code })
  • client.mfa.totpClient.getElevatedToken(code)
  • client.mfa.totpClient.removeMFAWithBackupCode(code)
  • client.passkey.login()
  • client.passkey.register({ userDisplayName })

Custom Subject Schema (Server-Side)

For server-side token verification, you can provide a custom subject schema to createOpenAuthsterClient.

import { createSubjects } from "@kagii/openauth/subject";
import * as v from "valibot";

const mySubjectSchema = createSubjects({
  user: v.object({
    id: v.string(),
    identifier: v.string(),
    role: v.nullable(v.string()),
    data: v.looseObject({}),
    clientID: v.string(),
    provider: v.string(),
  }),
});

const client = createOpenAuthsterClient({
  clientID: "my_project_01",
  issuerURI: "https://auth.yourdomain.com",
  redirectURI: "https://myapp.com/",
  secret: process.env.AUTH_SECRET,
  subject: mySubjectSchema,
});

await client.setTokenFromRequest(request);

If no subject schema is provided, the default OpenAuthster schema is used.


Server-Side Usage

On the server (API routes, Cloudflare Workers, etc.), you can still use createOpenAuthsterClient with a pre-set token from the request:

import { createOpenAuthsterClient } from "openauthster-shared/client/user";
import { createSubjects } from "@kagii/openauth/subject";
import * as v from "valibot";

const mySubjectSchema = createSubjects({
  user: v.object({
    id: v.string(),
    identifier: v.string(),
    role: v.nullable(v.string()),
    data: v.looseObject({}),
    clientID: v.string(),
    provider: v.string(),
  }),
});

export async function handleRequest(request: Request) {
  const client = createOpenAuthsterClient({
    clientID: "my_project_01",
    issuerURI: "https://auth.yourdomain.com",
    redirectURI: "https://myapp.com/",
    copyID: null,
    secret: process.env.AUTH_SECRET,
    subject: mySubjectSchema,
  });

  await client.setTokenFromRequest(request);

  if (!client.isAuthenticated) {
    return new Response("Unauthorized", { status: 401 });
  }

  // Read the private session
  const session = await client.getUserSession("private");

  return new Response(JSON.stringify(session), {
    headers: { "Content-Type": "application/json" },
  });
}

Server Helpers

| Method | Description | | ------------------------------- | --------------------------------------------------------------------------------- | | setTokenFromRequest(request) | Reads Authorization: Bearer … (or cookie), verifies and sets the client's token | | getTokenFromRequest(request) | Returns the bearer token string (or null) without setting it | | verify(token?) | Verifies token authenticity using the subject schema; returns true if valid | | getMetaData() | Returns lightweight token-derived user metadata without loading session data | | getUserById(user_id) | Fetches a user by ID | | getManyUserById(ids) | Fetches several users in a single request | | getUsers(filters?) | Fetches a user list and total count | | updateUserById(user_id, data) | Updates selected user fields | | setUserRoleById(user_id, role) | Updates only the user's role | | deleteUserById(user_id) | Deletes a user by ID | | deleteCurrentUser() | Deletes the currently authenticated user and clears local auth state |


Webhooks

Use the webhook helpers from openauthster-shared/webhook to verify incoming webhook requests from an OpenAuthster issuer and parse them into strongly typed payloads.

import { WebHook } from "openauthster-shared/webhook";

Handling an Incoming Webhook

This is the main integration point for applications receiving OpenAuthster webhook events:

import { WebHook } from "openauthster-shared/webhook";

export async function GET(request: Request) {
  const data = await WebHook.getWebHookPayloadFromRequest(
    "login_success",
    request,
    process.env.AUTH_SECRET as string,
  );

  console.log("webhook data", data);
  return new Response("ok");
}

getWebHookPayloadFromRequest() does all of the verification work for you:

  • Reads the payload from the payload query parameter for GET requests.
  • Reads the raw request body for non-GET requests such as POST.
  • Verifies the signature header using your app secret.
  • Rejects payloads older than 5 minutes to reduce replay risk.
  • Returns a typed payload matched to the event you requested.

If signature verification fails, or the timestamp is too old, it throws WebHookUnAuthorizedError.

Payload Shape

All webhook payloads follow this envelope:

type WebHookPayLoad<Event extends WebHookEvents, Data> = {
  event: Event;
  clientID: string;
  timestamp: string;
  id: string;
  data: Data;
  meta: {
    ip: string;
    userAgent: string;
  };
};

Example login_success payload:

{
  "event": "login_success",
  "clientID": "__my_project_xyz",
  "timestamp": "2026-04-02T12:00:00.000Z",
  "id": "webhook_uuid",
  "data": {
    "userID": "user_123",
    "provider": "google"
  },
  "meta": {
    "ip": "203.0.113.10",
    "userAgent": "Mozilla/5.0"
  }
}

Supported Events

The shared package currently defines these webhook events:

  • registration_success
  • login_success
  • code_sent
  • mfa_setup
  • mfa_update
  • mfa_confirmed
  • mfa_removed

The event-specific data payloads are:

  • registration_success: { userID, provider }
  • login_success: { userID, provider }
  • code_sent: { code, method, send_to, provider }
  • mfa_setup: { userID }
  • mfa_update: { userID, method: "backup_code" }
  • mfa_confirmed: { userID }
  • mfa_removed: { userID, method: "token" | "backup_code" }

If you want the payload types directly, import them from openauthster-shared/webhook/types:

import type {
  WebHookEvents,
  WebHookPayLoad,
  WebHookPayloadLoginSuccess,
  WebHookPayloadRegistrationSuccess,
} from "openauthster-shared/webhook/types";

Delivery Format

When OpenAuthster triggers a webhook:

  • GET webhooks receive the serialized payload in the payload query parameter.
  • POST webhooks receive the serialized payload as the request body.

Your receiving endpoint should always pass the original Request object directly to WebHook.getWebHookPayloadFromRequest(...) so the signature is verified against the unmodified payload.

Notes

  • Use the same app secret on the receiver that the issuer uses to sign webhook payloads.
  • The helper is intended for webhook consumers. Webhook registration and triggering are internal issuer-side capabilities.
  • If you need event names for UI or validation, WebHookEventsList and WebHookEventsDetails are exported from openauthster-shared/webhook/types.

Low-Level Client (openauthster-shared/client)

If you only need a raw @kagii/openauth client with OpenAuthster request defaults, use the low-level helpers:

import { createClient, createServerClient } from "openauthster-shared/client";

// Browser / Edge
const client = createClient({
  clientID: "my_project_01",
  issuer: "https://auth.yourdomain.com",
  copyID: "en-us", // or null
});

// Server-side
const serverClient = createServerClient({
  clientID: "my_project_01",
  issuer: "https://auth.yourdomain.com",
  request,
});

createServerClient() resolves client_id and copy_id from the request query string when present, then falls back to the explicit options you passed.

These helpers return the raw OpenAuth client. You are responsible for token storage, refresh, and higher-level session management yourself.


React Integration Example

See the openauth-react package for a higher-level React integration. Here's a minimal example:

import { createContext, useContext, useEffect, useRef, useState } from "react";
import {
  createOpenAuthsterClient,
  type OpenAuthsterClient,
} from "openauthster-shared/client/user";

// Create a context (use a global to survive HMR / StrictMode)
declare global {
  var __AUTH_CTX__: React.Context<OpenAuthsterClient<any, any, string, any>>;
}
globalThis.__AUTH_CTX__ ??= createContext(
  {} as OpenAuthsterClient<any, any, string, any>,
);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const client = useRef(
    createOpenAuthsterClient({
      clientID: "my_project_01",
      issuerURI: "https://auth.yourdomain.com",
      redirectURI: "http://localhost:3000/",
      copyID: "en-us",
    }),
  );

  useEffect(() => {
    void client.current.init().then(async () => {
      if (client.current.isAuthenticated) {
        await client.current.getUserSession("public");
      }
      await client.current.triggerUpdate();
    });
  }, []);

  return (
    <globalThis.__AUTH_CTX__.Provider value={client.current}>
      {children}
    </globalThis.__AUTH_CTX__.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(globalThis.__AUTH_CTX__);
  const key = useRef(crypto.randomUUID());
  const [, rerender] = useState(0);

  useEffect(() => {
    ctx.addInitializationListener(key.current, () => {
      rerender((value) => value + 1);
    });

    return () => {
      ctx.removeInitializationListener(key.current);
    };
  }, [ctx]);

  return ctx;
}

Then in a component:

function HomePage() {
  const auth = useAuth();

  if (!auth.isLoaded) return <p>Loading...</p>;

  return auth.isAuthenticated ? (
    <div>
      <p>Welcome, {auth.userMeta.identifier}</p>
      <pre>{JSON.stringify(auth.data.public, null, 2)}</pre>
      <button onClick={() => auth.logout()}>Logout</button>
    </div>
  ) : (
    <button onClick={() => auth.login()}>Login</button>
  );
}

Directory Map

openauthster-shared/          # workspace folder: openauthster-shared
├── index.ts                  # Main exports, provider registry, shared types, constants
├── utils.ts                  # Cookie and request helpers
├── client/
│   ├── index.ts              # Low-level createClient / createServerClient helpers
│   ├── user.ts               # OpenAuthsterClient + createOpenAuthsterClient
│   ├── storage.ts            # Auth storage adapter (browser or memory)
│   ├── file-storage.ts       # Owner-only file adapter for CLI tokens
│   └── cli.ts                # Loopback PKCE CLI client
│   └── mfa/                  # TOTP helpers used by the high-level client
├── client/passkey.ts         # Passkey helper used by the high-level client
├── database/
│   ├── drizzle.ts            # Drizzle exports and query helpers
│   ├── schema.ts             # Shared database schemas and table helpers
│   ├── endpoints.ts          # Shared endpoint response and filter types
│   └── delete-user.ts        # Shared delete-user cleanup helper
├── providers/                # Provider definitions and custom provider helpers
├── webhook/                  # Webhook helpers and types
└── security/                 # Shared security utilities

Related Repositories

License

License information coming soon.