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

@kaushik91/authrs-client

v0.0.11

Published

Typed TypeScript client for the Authrs multi-tenant authentication service

Readme

@kaushik91/authrs-client

Typed TypeScript client for the Authrs multi-tenant authentication service.

  • Zero external dependencies — uses native fetch (Node 18+, Next.js, Bun, Deno, browser)
  • Full TypeScript types for all requests and responses
  • Works in CJS and ESM projects

Installation

npm install @kaushik91/authrs-client
# or
pnpm add @kaushik91/authrs-client
# or
yarn add @kaushik91/authrs-client

Quick Start

import AuthrsClient from "@kaushik91/authrs-client";

const client = new AuthrsClient({
  baseUrl: "https://auth.example.com",
  tenantId: "my-tenant",
});

const session = await client.loginEmailPassword("[email protected]", "password");
console.log(session.sessionToken);

Configuration

The client can be configured via the constructor or environment variables.

| Option | Env var | Default | |--------|---------|---------| | baseUrl | AUTHRS_BASE_URL | http://localhost:3000 | | tenantId | AUTHRS_TENANT_ID | "" |

Constructor options take precedence over environment variables.

// From constructor
const client = new AuthrsClient({ baseUrl: "https://auth.example.com", tenantId: "acme" });

// From env vars (AUTHRS_BASE_URL, AUTHRS_TENANT_ID)
const client = new AuthrsClient();

Singleton helper (Next.js / server)

import { getAuthrsClient } from "@kaushik91/authrs-client";

const client = getAuthrsClient(); // returns cached instance
const client2 = getAuthrsClient({ baseUrl: "..." }); // creates new instance and caches it

Error Handling

All methods throw AuthrsError on non-2xx responses.

import { AuthrsError } from "@kaushik91/authrs-client";

try {
  await client.loginEmailPassword("[email protected]", "wrong-password");
} catch (err) {
  if (err instanceof AuthrsError) {
    console.error(err.message); // "Authrs POST /login/email-password → 401"
    console.error(err.status);  // 401
    console.error(err.body);    // parsed response body
  }
}

Types

interface AuthrsConfig {
  baseUrl?: string;
  tenantId?: string;
}

interface AuthrsUser {           // a per-tenant membership of a global identity
  id: string;                    // membership id
  identityId?: string;           // the shared identity (same human across tenants)
  email?: string;                // \
  mobile?: string;               //  } sourced from the identity
  countryCode?: string;          //  }
  firstName?: string;            //  }
  lastName?: string;             // /
  mfaEnabled?: boolean;          // from the identity
  username?: string;             // per-tenant
  status?: string;               // per-tenant
  isArchived?: boolean;
  accessValidUntil?: string | null;
  createdAt?: string;
  updatedAt?: string;
}

interface AuthrsSession {
  sessionToken: string;
  expiresAt?: string;  // RFC 3339, present on successful logins / tenant selection
  user?: AuthrsUser;
}

// --- Single sign-on (shared identity across tenants) ---

interface AuthrsTenantMembership {
  tenantId: string;
  status: string;
}

interface AuthrsIdentityLogin {
  identityToken: string;                  // short-lived; exchange via selectTenant
  tenants: AuthrsTenantMembership[];      // tenants this identity belongs to
}

interface AuthrsSessionInfo {             // GET /session/validate
  tenantId: string;
  userId: string;
  identityId: string;                     // global identity (same human across tenants)
  roles: string[];
  permissions: string[];
  expiresAt: string;
}

interface AuthrsMe {                      // GET /session/me
  userId: string;
  identityId: string;
  roles: string[];
  permissions: string[];
  expiresAt: string;
  user: AuthrsUser;
}

interface AuthrsVerificationPending {     // returned by signup on an existing-identity collision
  message: string;
}

interface AuthrsMembership {              // returned by verifyMembership
  id: string;
  tenantId: string;
  status: string;
}

interface AuthrsRole {
  id: string;
  name: string;
  uid?: string;
  tenantId?: string;
  createdAt?: string;
}

interface AuthrsPermissionStatement {
  sid?: string;
  effect: "Allow" | "Deny";
  principals: string[];
  actions: string[];
  resources: string[];
  conditions?: unknown[];
}

interface AuthrsPermissionDocument {
  version: string;
  statements: AuthrsPermissionStatement[];
}

interface AuthrsPermission {
  id: string;
  name: string;
  description?: string;
  document?: AuthrsPermissionDocument;
  tenantId?: string;
}

interface AuthrsPermissionCheckResult {
  resource: string;
  action?: string;
  allowed?: boolean;           // present when a single action is checked
  decisions?: Record<string, boolean>; // present when all actions are checked
}

interface AuthrsCreatePermissionParams {
  name: string;
  description?: string;
  document: AuthrsPermissionDocument;
}

interface AuthrsPackageSyncParams {
  packageId: string;
  tables: string[];
  customActions?: string[];
}

interface AuthrsKvEntry {
  groupKey: string;
  key: string;
  value: string;
}

interface AuthrsCreateUserParams {
  firstName?: string;
  lastName?: string;
  email?: string;
  username?: string;
  mobile?: string;
  countryCode?: string;
  password?: string;
  retypePassword?: string;
}

interface AuthrsGroup {
  id: string;
  name: string;
  uid: string;
  description?: string | null;
  tenantId?: string;
}

interface AuthrsCreateGroupParams {
  name: string;
  description?: string;
}

Methods

Health

healthCheck()

Checks if the Authrs server is up. Does not require a tenant ID or auth token.

const { status } = await client.healthCheck();
// { status: "ok" }

getMetrics()

Returns Prometheus-style metrics for the Authrs server. Does not require a tenant ID or auth token.

const metrics = await client.getMetrics();

getSpec()

Returns the OpenAPI/Swagger spec for the Authrs server.

const spec = await client.getSpec();

Authentication

These auth methods are tenant-scoped (the configured tenantId is sent automatically). For single sign-on across tenants (one identity, many tenants), see Single Sign-On below.

signup(firstName, lastName, email, password, retypePassword)

Register a new user in the tenant. The email/mobile is a global identity handle: if it already belongs to an existing identity (e.g. the user is registered in another tenant), no account is created — a verify-to-join email is sent to the owner and an AuthrsVerificationPending is returned instead. Disambiguate with "id" in result.

const result = await client.signup("Jane", "Doe", "[email protected]", "s3cr3t!", "s3cr3t!");
if ("id" in result) {
  // AuthrsUser — created
} else {
  console.log(result.message); // AuthrsVerificationPending — check your email to join
}

verifyMembership(token)

Accept a verify-to-join invite using the token from the signup email. Adds the existing identity as a member of the inviting tenant. The tenant is encoded in the token — no X-Tenant-ID needed.

const membership = await client.verifyMembership("token-from-email");
// { id, tenantId, status }

loginEmailPassword(email, password)

Log in with email and password. Returns a session token.

const session = await client.loginEmailPassword("[email protected]", "s3cr3t!");
// { sessionToken: "...", user: { ... } }

loginUsernamePassword(username, password)

Log in with a username and password.

const session = await client.loginUsernamePassword("jdoe", "s3cr3t!");

loginEmailOtpRequest(email)

Request an OTP sent to the user's email.

await client.loginEmailOtpRequest("[email protected]");

loginMobileOtpRequest(mobile, countryCode)

Request an OTP sent via SMS to the user's mobile number.

await client.loginMobileOtpRequest("9876543210", "+91");

loginWhatsAppOtpRequest(mobile, countryCode)

Request an OTP sent via WhatsApp.

await client.loginWhatsAppOtpRequest("9876543210", "+91");

loginOtpVerify(identifier, code, channel)

Verify an OTP and receive a session token. channel is "email", "sms", or "whatsapp".

const session = await client.loginOtpVerify("[email protected]", "123456", "email");
// returns AuthrsSession

oauthAuthorizeUrl(provider)

Returns the OAuth authorization URL for the given provider (e.g. "google", "github"). Redirect the user to this URL to start the OAuth flow.

const url = client.oauthAuthorizeUrl("google");
// "https://auth.example.com/oauth/google"
window.location.href = url;

This is a synchronous method — it does not make a network request.

oauthCallback(provider, code, state?, token?)

Completes the OAuth flow by exchanging the authorization code returned by the provider. Call this from your OAuth callback route.

const session = await client.oauthCallback("google", req.query.code, req.query.state);
// returns AuthrsSession

forgotPassword(email)

Sends a password reset email. Operates on the global identity — the reset (and the new password) applies across every tenant the identity belongs to, not just the current one.

await client.forgotPassword("[email protected]");

resetPassword(token, newPassword, retypePassword)

Resets the (global) password using the token from the reset email.

await client.resetPassword("reset-token-from-email", "newPass!", "newPass!");

Single Sign-On (SSO)

The same person can belong to many tenants under one identity and one credential. These endpoints are not tenant-scoped — they don't send X-Tenant-ID. Log in once by email, then pick a tenant (or switch between tenants) without re-entering the password.

// 1) Authenticate the identity (no tenant). Returns a short-lived identity token + tenants.
const { identityToken, tenants } = await client.loginIdentity("[email protected]", "s3cr3t!");
// tenants: [{ tenantId: "acme", status: "active" }, { tenantId: "globex", status: "active" }]

// 2) Exchange the identity token for a session in the chosen tenant.
const session = await client.selectTenant(identityToken, "acme");
localStorage.setItem("token", session.sessionToken);

// later — hop to another tenant from an active session, no re-auth:
const other = await client.switchTenant(session.sessionToken, "globex");

loginIdentity(email, password)

Tenant-less login by email. Returns an AuthrsIdentityLogin ({ identityToken, tenants }).

identityTenants(identityToken)

Lists the tenants for an identity token (the SSO tenant picker). Bearer = identity token.

const { tenants } = await client.identityTenants(identityToken);

selectTenant(identityToken, tenantId)

Exchanges an identity token for a tenant-scoped session. Returns an AuthrsSession.

switchTenant(sessionToken, tenantId)

Mints a session for another tenant the same identity belongs to, from an active session — no re-authentication.

logoutGlobal(token)

Revokes every session of this identity across all of its tenants (the "log out everywhere" button).

await client.logoutGlobal(session.sessionToken);

Availability Checks

These methods check whether a given identifier is already registered. They require a valid session token. Email and mobile are checked globally (an identity handle is unique across all tenants); username is per-tenant.

checkEmailAvailability(token, email)

Returns { available: true } if the email is not yet registered in the tenant.

const { available } = await client.checkEmailAvailability("session-token", "[email protected]");
if (!available) {
  console.error("Email is already taken");
}

checkUsernameAvailability(token, username)

Returns { available: true } if the username is not yet registered in the tenant.

const { available } = await client.checkUsernameAvailability("session-token", "jdoe");

checkMobileAvailability(token, mobile, countryCode)

Returns { available: true } if the mobile number (with country code) is not yet registered in the tenant.

const { available } = await client.checkMobileAvailability("session-token", "9876543210", "+91");

Session

Session methods use a bearer token. Most do not send a X-Tenant-ID header (the session is global).

validateSession(token)

Validates a session token and returns its AuthrsSessionInfo (tenantId, userId, identityId, roles, permissions, expiresAt). Throws AuthrsError (401) if the token is invalid or expired.

const info = await client.validateSession("session-token");
// { tenantId, userId, identityId, roles, permissions, expiresAt }

me(token)

Returns the current session info plus the user profile (AuthrsMe). Includes identityId.

const me = await client.me("session-token");
// { userId, identityId, roles, permissions, expiresAt, user: { ... } }

changePassword(token, currentPassword, newPassword, retypePassword)

Changes the authenticated user's password.

await client.changePassword("session-token", "oldPass!", "newPass!", "newPass!");

logout(token)

Invalidates the current session.

await client.logout("session-token");

logoutAll(token)

Invalidates all sessions for the current user across all devices. This call is tenant-scoped.

await client.logoutAll("session-token");

forceChangePassword(changeToken, newPassword, retypePassword)

Completes a forced password change. When a login response includes passwordChangeRequired: true, the server returns a changeToken instead of a full session token. Pass that token here to set a new password and receive a valid session.

const login = await client.loginEmailPassword("[email protected]", "temp-password");
// login.passwordChangeRequired === true
const session = await client.forceChangePassword(login.changeToken, "newPass!", "newPass!");
// session.sessionToken is now a valid full session token

MFA (Multi-Factor Authentication)

MFA methods require a session token and are tenant-scoped.

enableMfa(token)

Initiates MFA setup for the authenticated user (e.g. returns a TOTP QR code URI).

const result = await client.enableMfa("session-token");

verifyMfa(token, code)

Confirms and activates MFA using the setup code.

await client.verifyMfa("session-token", "123456");

validateMfa(token, code)

Validates an MFA code during login (second factor step).

await client.validateMfa("session-token", "123456");

Admin — Users

All admin methods require an admin session token.

createUser(token, params)

Creates a new user. Fields are all optional so you can create users with any combination of identifier (email, username, or mobile).

const user = await client.createUser("admin-token", {
  firstName: "John",
  lastName: "Smith",
  email: "[email protected]",
  password: "tempPass!",
  retypePassword: "tempPass!",
});

listUsers(token, options?)

Lists all users in the tenant. Pass { includeArchived: true } to include soft-deleted users.

const { users } = await client.listUsers("admin-token");
const { users: all } = await client.listUsers("admin-token", { includeArchived: true });

archiveUser(token, userId)

Soft-deletes a user (sets isArchived: true).

await client.archiveUser("admin-token", "user-id");

resetUserPassword(token, userId, newPassword, retypePassword, forcePasswordChange?)

Resets a user's password as an admin (no current password required). Set forcePasswordChange: true to require the user to change their password on next login.

await client.resetUserPassword("admin-token", "user-id", "newPass!", "newPass!");
// Force user to change password on next login:
await client.resetUserPassword("admin-token", "user-id", "tempPass!", "tempPass!", true);

setAccessValidity(token, userId, accessValidUntil)

Sets or clears the access expiry for a user. Once the datetime passes the user cannot log in and receives a 403 access_expired error. Pass null to remove the expiry (access becomes indefinite).

// Restrict access until end of year
await client.setAccessValidity("admin-token", "user-id", "2026-12-31T23:59:59Z");

// Remove expiry — access is indefinite
await client.setAccessValidity("admin-token", "user-id", null);

Admin — Roles

createRole(token, name, parentRoleId?)

Creates a new role. Pass an optional parentRoleId to make the role inherit all permissions from a parent role (role hierarchy).

const role = await client.createRole("admin-token", "editor");
// returns AuthrsRole

// Create a sub-role that inherits from an existing role
const subRole = await client.createRole("admin-token", "junior-editor", role.id);

listRoles(token)

Lists all roles in the tenant. Each role includes parentRoleId (null for root roles).

const { roles } = await client.listRoles("admin-token");

setRoleParent(token, roleId, parentRoleId)

Sets or clears a role's parent. Pass null to make the role a root role. A role inherits every permission attached to its ancestors. Cycles are rejected by the server.

await client.setRoleParent("admin-token", "role-id", "parent-role-id");
await client.setRoleParent("admin-token", "role-id", null); // detach

getRoleHierarchy(token, roleId)

Returns the ancestor chain for a role, root-first.

const { ancestors } = await client.getRoleHierarchy("admin-token", "role-id");
// ancestors: [{ id, name, uid }, ...]

listUserRoles(token, userId)

Lists roles assigned to a specific user.

const { roles } = await client.listUserRoles("admin-token", "user-id");

assignRole(token, userId, roleId)

Assigns a role to a user.

await client.assignRole("admin-token", "user-id", "role-id");

removeRole(token, userId, roleId)

Removes a role from a user.

await client.removeRole("admin-token", "user-id", "role-id");

listRolePermissions(token, roleId)

Lists all permissions attached to a role.

const { permissions } = await client.listRolePermissions("admin-token", "role-id");

attachPermissionToRole(token, roleId, permissionId)

Attaches a permission to a role. The policy takes effect on the next request (cache evicted).

await client.attachPermissionToRole("admin-token", "role-id", "permission-id");

detachPermissionFromRole(token, roleId, permissionId)

Removes a permission from a role.

await client.detachPermissionFromRole("admin-token", "role-id", "permission-id");

Admin — Groups

Groups combine users for shared role assignments, Cedar policy targeting, and bulk operations. Roles assigned to a group are inherited by all members — their session payload includes the full effective set.

createGroup(token, params)

Creates a new group. The uid (slug) is derived automatically from the name.

const group = await client.createGroup("admin-token", {
  name: "Engineering",
  description: "All engineering team members",
});
// returns AuthrsGroup: { id, name, uid: "engineering", description }

listGroups(token)

Lists all groups in the tenant.

const { groups } = await client.listGroups("admin-token");

getGroup(token, groupId)

Gets a single group by ID.

const group = await client.getGroup("admin-token", "group-id");

deleteGroup(token, groupId)

Deletes a group. Members are unlinked and inherited roles are removed from their sessions on next login.

await client.deleteGroup("admin-token", "group-id");

addUserToGroup(token, groupId, userId)

Adds a user to a group. Both must exist in the tenant. Idempotent — adding an existing member is a no-op.

await client.addUserToGroup("admin-token", "group-id", "user-id");

listGroupMembers(token, groupId)

Lists all user IDs that are members of a group.

const { users } = await client.listGroupMembers("admin-token", "group-id");
// users: ["uuid1", "uuid2", ...]

removeUserFromGroup(token, groupId, userId)

Removes a user from a group.

await client.removeUserFromGroup("admin-token", "group-id", "user-id");

listUserGroups(token, userId)

Lists all groups a user belongs to.

const { groups } = await client.listUserGroups("admin-token", "user-id");
// groups: [{ id, name, uid }, ...]

assignRoleToGroup(token, groupId, roleId)

Assigns a role to a group. All current and future members inherit this role. Idempotent.

await client.assignRoleToGroup("admin-token", "group-id", "role-id");

listGroupRoles(token, groupId)

Lists roles assigned to a group.

const { roles } = await client.listGroupRoles("admin-token", "group-id");

removeRoleFromGroup(token, groupId, roleId)

Removes a role assignment from a group.

await client.removeRoleFromGroup("admin-token", "group-id", "role-id");

Admin — Permissions

Permissions are Cedar policy documents that control what actions users (via their roles) can perform on resources.

createPermission(token, params)

Creates a new Cedar permission policy. The document field is validated before saving.

Principals accept: role:<uid>, role:<name>, user:<uuid>, user:<email>, user:<username>, or *.

const permission = await client.createPermission("admin-token", {
  name: "allow-editor-read-materials",
  description: "Allow editors to read materials",
  document: {
    version: "1.0",
    statements: [
      {
        sid: "AllowEditorReadMaterials",
        effect: "Allow",
        principals: ["role:editor"],
        actions: ["getMaterials"],
        resources: ["service:core/package:manufacturing_core/table:materials"],
        conditions: [],
      },
    ],
  },
});
// returns AuthrsPermission

listPermissions(token)

Lists all permissions in the tenant.

const { permissions } = await client.listPermissions("admin-token");

getPermission(token, permissionId)

Gets a single permission by ID.

const permission = await client.getPermission("admin-token", "permission-id");

deletePermission(token, permissionId)

Deletes a permission and evicts the tenant's policy cache.

await client.deletePermission("admin-token", "permission-id");

checkPermission(token, userId, resource, action?, context?)

Evaluates Cedar policy for a user against a resource.

  • Pass action to check a single action → returns { resource, action, allowed: boolean }.
  • Omit action to check all actions for the resource scope → returns { resource, decisions: { actionName: boolean, ... } }.

Resource scopes: service:core/package:pkg/table:tbl (table), service:core/package:pkg (package), service:core (service).

// Single action
const result = await client.checkPermission(
  "admin-token",
  "user-id",
  "service:core/package:manufacturing_core/table:materials",
  "getMaterials",
);
// { resource: "...", action: "getMaterials", allowed: true }

// All actions for a table
const result = await client.checkPermission(
  "admin-token",
  "user-id",
  "service:core/package:manufacturing_core/table:materials",
);
// { resource: "...", decisions: { getMaterials: true, postMaterials: false, ... } }

Admin — Packages

syncPackage(token, params)

Registers or updates a package's tables and custom actions. Rebuilds the Cedar schema and evicts all policy caches. Standard CRUD actions (getMaterials, postMaterials, etc.) are auto-generated from table names. Called automatically by architect-sdk after a package install.

await client.syncPackage("admin-token", {
  packageId: "manufacturing_core",
  tables: ["materials", "bom_headers", "bom_lines"],
  customActions: ["approveBom", "rejectBom"],
});

Admin — KV Store

A key-value store scoped to the tenant, useful for storing per-tenant configuration.

listKvKeys(token)

Lists all KV entries for the tenant.

const entries = await client.listKvKeys("admin-token");
// returns AuthrsKvEntry[]

getKvValue(token, groupKey, key)

Gets a single KV entry by group and key.

const entry = await client.getKvValue("admin-token", "config", "theme");
// { groupKey: "config", key: "theme", value: "dark" }

setKvValue(token, groupKey, key, value)

Creates or updates a KV entry.

await client.setKvValue("admin-token", "config", "theme", "dark");

deleteKvValue(token, groupKey, key)

Deletes a KV entry.

await client.deleteKvValue("admin-token", "config", "theme");

Common Patterns

Store the session token after login

const session = await client.loginEmailPassword(email, password);
localStorage.setItem("token", session.sessionToken);

Single sign-on across tenants

// Log in once; let the user choose which tenant to enter.
const { identityToken, tenants } = await client.loginIdentity(email, password);

if (tenants.length === 1) {
  const session = await client.selectTenant(identityToken, tenants[0].tenantId);
  localStorage.setItem("token", session.sessionToken);
} else {
  // render a tenant picker from `tenants`, then:
  const session = await client.selectTenant(identityToken, chosenTenantId);
  localStorage.setItem("token", session.sessionToken);
}

Middleware / route guard (Next.js)

import { getAuthrsClient } from "@kaushik91/authrs-client";

export async function middleware(request: Request) {
  const token = request.headers.get("Authorization")?.replace("Bearer ", "");
  if (!token) return new Response("Unauthorized", { status: 401 });

  try {
    await getAuthrsClient().validateSession(token); // throws AuthrsError if invalid/expired
  } catch {
    return new Response("Unauthorized", { status: 401 });
  }
}

OTP login flow

// Step 1: request OTP
await client.loginEmailOtpRequest("[email protected]");

// Step 2: user enters code from email
const session = await client.loginOtpVerify("[email protected]", userEnteredCode, "email");

MFA login flow

// Initial login returns a partial/pre-MFA session token
const session = await client.loginEmailPassword(email, password);

// Validate MFA to fully authenticate
await client.validateMfa(session.sessionToken, totpCode);

Forced password change flow

const login = await client.loginEmailPassword(email, temporaryPassword);

if (login.passwordChangeRequired) {
  // Redirect user to a change-password screen, then:
  const session = await client.forceChangePassword(login.changeToken, newPassword, newPassword);
  // session.sessionToken is now a valid full session
}

Environment Variables

| Variable | Description | |----------|-------------| | AUTHRS_BASE_URL | Base URL of the Authrs server | | AUTHRS_TENANT_ID | Tenant identifier sent as X-Tenant-ID header |