@kaushik91/authrs-client
v0.0.11
Published
Typed TypeScript client for the Authrs multi-tenant authentication service
Maintainers
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-clientQuick 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 itError 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 AuthrsSessionoauthAuthorizeUrl(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 AuthrsSessionforgotPassword(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 tokenMFA (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); // detachgetRoleHierarchy(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 AuthrsPermissionlistPermissions(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
actionto check a single action → returns{ resource, action, allowed: boolean }. - Omit
actionto 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 |
