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.19

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, { isLoginSuccess } from "@kaushik91/authrs-client";

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

const result = await client.loginEmailPassword("[email protected]", "password");
if (isLoginSuccess(result)) {
  console.log(result.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;
}

// --- Login result (what the login-style endpoints actually return) ---

interface AuthrsLoginSuccess {            // credentials OK, session issued
  sessionToken: string;
  expiresAt: string;                      // RFC 3339
}

interface AuthrsMfaRequired {             // credentials OK, MFA challenge
  mfaRequired: true;
  mfaToken: string;
}

interface AuthrsPasswordChangeRequired {  // credentials OK, must set a new password first
  passwordChangeRequired: true;
  changeToken: string;
}

// --- OAuth / MFA ---

type AuthrsOAuthProvider = "google" | "microsoft";

interface AuthrsOAuthStart {       // startOAuth()
  authorizationUrl: string;        // navigate the browser here
  state: string;                   // keep; compare with the state the provider sends back
}

interface AuthrsMfaStatus { enabled: boolean; backupCodesRemaining: number; }
interface AuthrsMfaEnrollment { secret: string; otpauthUrl: string; }   // enableMfa()
interface AuthrsMfaEnabled { enabled: true; backupCodes: string[]; }    // verifyMfa()

type AuthrsLoginResult =
  | AuthrsLoginSuccess
  | AuthrsMfaRequired
  | AuthrsPasswordChangeRequired;

// Narrowing helpers (also exported):
//   isLoginSuccess(r), isMfaRequired(r), isPasswordChangeRequired(r)

interface AuthrsListQuery {               // shared by RSQL-capable list endpoints
  q?: string;                             // RSQL filter, e.g. "status==active"
  sort?: string;                          // e.g. "createdAt,desc"
  limit?: number;                         // page size (server-clamped)
  offset?: number;                        // pagination offset
}

// --- 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;
  parentRoleId?: string | null;  // set when the role inherits from a parent
  tenantId?: string;
  createdAt?: string;
}

interface AuthrsPermissionStatement {
  sid: string;
  effect: "allow" | "deny";      // lowercase — authrs rejects "Allow"/"Deny"
  principals: string[];
  actions: string[];
  resources: string[];            // exact paths, or patterns with `*` (e.g. ".../query:*")
  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;
  overwrite?: boolean;  // replace an existing same-name permission in place (idempotent sync)
}

interface AuthrsPackageSyncParams {
  packageId: string;
  tables: string[];
  extensibleTables?: string[];  // tables with an `extensible` JSON column → get/put/delete actions
  customActions?: string[];
}

interface AuthrsPackageInfo {    // returned by listPackageActions
  packageId: string;
  tables: string[];
  actions: string[];
}

interface AuthrsBuiltinAction {  // returned by listPackageActions (builtinActions)
  action: string;                // e.g. "run"
  resourceType: string;          // e.g. "query"
}

interface AuthrsTenant {         // returned by listTenants (builder-only)
  id: string;
  name: string;
  status: string;
  createdAt: 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;
}

interface AuthrsUpdateGroupParams {
  name?: string;
  description?: string | null; // null clears the description
}

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, opts?)

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.

opts may include mobile and countryCode to attach a phone number to the identity.

const result = await client.signup("Jane", "Doe", "[email protected]", "s3cr3t!", "s3cr3t!");
// with a phone number:
// await client.signup("Jane", "Doe", "[email protected]", "s3cr3t!", "s3cr3t!", { mobile: "9876543210", countryCode: "+91" });
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 an AuthrsLoginResult — one of three shapes: a session (sessionToken/expiresAt), an MFA challenge (mfaRequired/mfaToken), or a forced password change (passwordChangeRequired/changeToken). Narrow with the exported isLoginSuccess / isMfaRequired / isPasswordChangeRequired guards.

import { isLoginSuccess, isPasswordChangeRequired } from "@kaushik91/authrs-client";

const result = await client.loginEmailPassword("[email protected]", "s3cr3t!");
if (isLoginSuccess(result)) {
  console.log(result.sessionToken, result.expiresAt);
} else if (isPasswordChangeRequired(result)) {
  const session = await client.forceChangePassword(result.changeToken, "newpass!", "newpass!");
} else {
  // result.mfaRequired — complete the MFA challenge with result.mfaToken
}

loginUsernamePassword(username, password)

Log in with a username and password. Returns an AuthrsLoginResult (see loginEmailPassword).

const result = 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 complete login. channel is "email", "sms", or "whatsapp". Returns an AuthrsLoginResult (see loginEmailPassword).

const result = await client.loginOtpVerify("[email protected]", "123456", "email");

startOAuth(provider)

Starts a Google or Microsoft sign-in for the current tenant. Returns { authorizationUrl, state }. Keep state, then send the browser to authorizationUrl. The provider redirects back to the tenant's configured redirectUrl (a page in your app) with ?code=…&state=….

const { authorizationUrl, state } = await client.startOAuth("microsoft");
sessionStorage.setItem("oauth_state", state);
window.location.href = authorizationUrl;

Provider credentials are configured per tenant in the Authrs kv_store (group oauth, key google / microsoft): { clientId, clientSecret, redirectUrl, tenant?, allowSignup? }. For Microsoft, tenant is common (default), organizations, consumers, or an Entra tenant ID.

oauthCallback(provider, code, state)

Completes the sign-in from your redirectUrl page. The tenant comes from state, so no X-Tenant-ID is sent. Returns an AuthrsLoginResult, so the account may still require MFA.

const params = new URLSearchParams(window.location.search);
if (params.get("state") !== sessionStorage.getItem("oauth_state")) throw new Error("State mismatch");
const result = await client.oauthCallback("microsoft", params.get("code")!, params.get("state")!);
if (isMfaRequired(result)) {
  // prompt for the authenticator code, then client.validateMfa(result.mfaToken, code)
}

The first sign-in links the provider account to the existing identity with the same email when the provider has verified that email. Otherwise it creates a new password-less identity, unless the tenant sets allowSignup: false.

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.
//    Returns an AuthrsLoginResult — the tenant may still require MFA / a password change.
const result = await client.selectTenant(identityToken, "acme");
if (!isLoginSuccess(result)) throw new Error("MFA or password change required");
localStorage.setItem("token", result.sessionToken);

// later — hop to another tenant from an active session, no re-auth:
const other = await client.switchTenant(result.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 AuthrsLoginResult (the target tenant may require MFA or a forced password change).

switchTenant(sessionToken, tenantId)

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

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.

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

const login = await client.loginEmailPassword("[email protected]", "temp-password");
if (isPasswordChangeRequired(login)) {
  const result = await client.forceChangePassword(login.changeToken, "newPass!", "newPass!");
  // a session, or { mfaRequired, mfaToken } if the account has MFA enabled
}

MFA (Multi-Factor Authentication)

Authenticator-app TOTP (Google/Microsoft Authenticator, Authy, 1Password, …) with single-use backup codes. MFA belongs to the global identity, so it applies in every tenant and to every login method (password, OTP, OAuth, SSO). These methods send no X-Tenant-ID.

mfaStatus(token)

const { enabled, backupCodesRemaining } = await client.mfaStatus(sessionToken);

enableMfa(token)

Starts enrollment. Render otpauthUrl as a QR code (e.g. with the qrcode package) and show secret for manual entry. MFA is not active until verifyMfa succeeds.

const { secret, otpauthUrl } = await client.enableMfa(sessionToken);

verifyMfa(token, code)

Confirms enrollment with the first code from the app and turns MFA on. Returns 10 backup codes. Show them once: they cannot be retrieved later.

const { backupCodes } = await client.verifyMfa(sessionToken, "123456");

validateMfa(mfaToken, code)

Completes a login that returned { mfaRequired: true, mfaToken }. code is the current 6-digit TOTP code or a backup code. The mfaToken lasts 5 minutes or 5 attempts, and wrong codes count toward the account lockout.

const { sessionToken, expiresAt } = await client.validateMfa(result.mfaToken, "123456");

disableMfa(token, code) / regenerateBackupCodes(token, code)

Both require a current TOTP or backup code.

await client.disableMfa(sessionToken, "123456");
const { backupCodes } = await client.regenerateBackupCodes(sessionToken, "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 users in the tenant. options accepts includeArchived plus the shared AuthrsListQuery fields (q, sort, limit, offset) for RSQL filtering, sorting, and pagination.

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

// filter, sort, and paginate
const page = await client.listUsers("admin-token", {
  q: "status==active",
  sort: "createdAt,desc",
  limit: 20,
  offset: 0,
});

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, options?)

Lists roles in the tenant. Each role includes parentRoleId (null for root roles). Accepts an optional AuthrsListQuery (q, sort, limit, offset).

const { roles } = await client.listRoles("admin-token");
const { roles: named } = await client.listRoles("admin-token", { q: "name==editor*" });

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, options?)

Lists groups in the tenant. Accepts an optional AuthrsListQuery (q, sort, limit, offset).

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");

updateGroup(token, groupId, params)

Partially updates a group. Omitted fields are left unchanged; pass description: null to clear the description. Renaming does not change the group's uid, so Cedar policies that reference the group keep working.

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

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, options?)

Lists the members of a group as full AuthrsUser objects. Accepts an optional AuthrsListQuery (q, sort, limit, offset).

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

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. Pass overwrite: true to replace an existing permission with the same name in place (idempotent re-sync) instead of failing on the unique-name constraint.

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

effect must be lowercase ("allow" / "deny"), and every statement needs a sid.

Wildcard resources. A resource containing * is a pattern matched against the full resource path; * matches any characters, including /. The action must still match exactly.

| Resource | Covers | |---|---| | service:core/package:pkg/query:sample | only that query | | service:core/package:pkg/query:* | every query in pkg | | service:core/package:pkg/* | everything in pkg (tables, columns, queries) | | service:core/* | everything in the service | | * | any resource |

Deny statements win over allow, so a wildcard allow can be narrowed with an exact deny (and vice versa).

// Let a role run every saved query in architect-sdk's standalone (_default) query set.
await client.createPermission("admin-token", {
  name: "run-all-queries",
  document: {
    version: "1.0",
    statements: [
      {
        sid: "RunAllQueries",
        effect: "allow",
        principals: ["*"],
        actions: ["run"],
        resources: ["service:core/package:_default/query:*"],
        conditions: [],
      },
    ],
  },
});

listPermissions(token, options?)

Lists permissions in the tenant. Accepts an optional AuthrsListQuery (q, sort, limit, offset).

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/query:q (saved query — only run), 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.

List any tables that expose extensible JSON columns in extensibleTables (a subset of tables); authrs derives get/put/deleteExtensibleFields<Table> actions for them.

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

listPackageActions(token)

Lists every registered package with its synced tables and derived actions. Useful for building a permission editor or discovering the available Cedar actions.

const { packages } = await client.listPackageActions("admin-token");
// packages: [{ packageId: "manufacturing_core", tables: [...], actions: [...] }, ...]
// builtinActions: [{ action: "run", resourceType: "query" }]

builtinActions are declared for every tenant without any sync — today just run, which applies only to saved-query resources (service:<svc>/package:<pkg>/query:<id>). Don't pass them as customActions. Older servers omit the field.


Platform (builder-only)

listTenants(token)

Lists every tenant on the server. Restricted to a session in the builder tenant that holds an allowed builder role — other callers receive a 403 (AuthrsError). The client's configured tenantId must be the builder tenant.

const { tenants } = await client.listTenants("builder-session-token");
// tenants: [{ id, name, status, createdAt }, ...]

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

const result = await client.loginEmailPassword(email, password);   // or OTP / OAuth / selectTenant

if (isMfaRequired(result)) {
  // Ask for the authenticator code (or a backup code)
  const session = await client.validateMfa(result.mfaToken, totpCode);
  // session.sessionToken is a full session
}

Authenticator enrollment flow

const { otpauthUrl, secret } = await client.enableMfa(sessionToken);
// show QR(otpauthUrl) + secret; user scans and types the first code
const { backupCodes } = await client.verifyMfa(sessionToken, firstCode);
// show backupCodes once

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 |