@kaushik91/authrs-client
v0.0.19
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, { 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 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;
}
// --- 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); // 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, 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 AuthrsPermissioneffect 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
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/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 onceForced 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 |
