authz-gate
v1.2.0
Published
A TypeScript-first authorization library that provides a flexible, extensible, and strongly typed system for defining and enforcing access control. Supports both Gates (global abilities) and Policies (resource-specific logic) with full type inference and
Maintainers
Readme
authz-gate
A Laravel-inspired, framework-agnostic authorization system for Node.js, offering type-safe Gates & Policies for granular access control.
A TypeScript-first authorization library that provides a flexible, extensible, and strongly typed system for defining and enforcing access control. Supports both Gates (global abilities) and Policies (resource-specific logic) with full type inference and modern async/await support.
✨ Features
- 🎯 Type-Safe Authorization: Full TypeScript support with parameter inference and autocompletion
- 🔗 Fluent Builder API: Clean, readable ability definitions via
initialize()function - 📓 Declarative API: Define rules once, enforce everywhere with consistent behavior
- 🏗️ Policy System: Resource-specific authorization logic with automatic method resolution
- ⚡ Async Support: Both synchronous and asynchronous ability callbacks
- 🔄 Lifecycle Hooks:
before()andafter()hooks for global and policy-level authorization - 🧩 Framework Agnostic: Core logic independent of any HTTP framework
- 📦 Zero Dependencies: No external dependencies in core library
- 🔍 Strict Validation: Compile-time ability name validation and runtime checks
📦 Installation
npm install authz-gate
# or
yarn add authz-gate
# or
pnpm add authz-gate// ES Modules
import { initialize, Policy } from 'authz-gate';
// CommonJS
const { initialize, Policy } = require('authz-gate');🚀 Quick Start
Simple Authorization
import { initialize } from 'authz-gate';
// Define your abilities
const gate = initialize(keeper =>
keeper
.define('view-posts', (user: User) => user.hasPermission('read'))
.define('create-post', (user: User) => user.role === 'editor')
.define('edit-post', (user: User, post: Post) => user.id === post.authorId)
.define('delete-post', (user: User, post: Post, isPublished: boolean) => user.id === post.authorId && !isPublished)
.define('publish-post', async (user: User) => await checkUserHasAdminPermission(user.id))
);
// Use anywhere in your app
const canEdit = await gate.allows('edit-post', user, post);
const cannotDelete = await gate.denies('delete-post', user, post, true);
if (canEdit) {
// User is authorized to edit the post
}Best Practice: Define your gate in a separate module (e.g.,
./auth/gate.ts) and export it for consistent use across your application.
🔧 Core API
Basic Permission Checks
// Single permission check
const allowed = await gate.allows('edit-post', user, post);
const denied = await gate.denies('edit-post', user, post);Batch Permission Checks
Check multiple permissions at once:
// All permissions must pass
const hasAllPermissions = await gate.all({
'view-posts': [user],
'edit-post': [user, post]
});
// Any permission can pass
const hasAnyPermission = await gate.any({
'delete-post': [user, post, false],
'moderate-content': [user]
});
// No permissions should pass
const hasNoAdminRights = await gate.none({
'delete-post': [user, post, false],
'admin-panel': [user]
});
// Alternative tuple syntax
const canPerformActions = await gate.allOf([
['view-posts', user],
['edit-post', user, post]
]);
const canModerate = await gate.anyOf([
['delete-post', user, post],
['moderate-content', user]
]);
const isNotAdmin = await gate.noneOf([
['admin-panel', user],
['delete-user', user, otherUser]
]);🏗️ Policy System
For resource-specific authorization logic, use Policy classes. Policies automatically map to abilities using the resource:method pattern:
import { initialize, Policy } from 'authz-gate';
// Define a Policy class for Message resource
class MessagePolicy extends Policy {
// Optional: Policy-level before hook
before(ability: string, user: User, message?: Message) {
// Message authors can perform any action on their messages
if (message && user.id === message.authorId) return true;
// Continue to specific method
return null;
}
view(user: User, message: Message, isPublic?: boolean): boolean {
// Public messages or same organization
return isPublic || user.organizationId === message.organizationId;
}
delete(user: User): boolean {
// Only admins can delete (author check handled by before hook)
return user.role === 'admin';
}
}
// Register the policy
const gate = initialize(keeper =>
keeper
.policy('message', new MessagePolicy())
// Still can define global abilities
.define('create-message', (user: User) => user.isVerified)
);
// Usage - calls MessagePolicy methods automatically
const canView = await gate.allows('message:view', user, message);
const canDelete = await gate.allows('message:delete', user, message);
const canCreate = await gate.allows('create-message', user);🔥 Advanced Examples
Role-Based Access Control (RBAC)
interface User {
id: string;
role: 'admin' | 'moderator' | 'user';
permissions: string[];
}
const gate = initialize(keeper =>
keeper
.before((ability, user) => {
// Admins can do everything
if (user.role === 'admin') return true;
return null;
})
.define('moderate-content', (user: User) =>
['admin', 'moderator'].includes(user.role)
)
.define('view-admin-panel', (user: User) => user.role === 'admin')
);Attribute-Based Access Control (ABAC)
const gate = initialize(keeper =>
keeper
.before((ability, user, resource) => {
// Tenant isolation
if (resource?.tenantId && user.tenantId !== resource.tenantId) {
return false;
}
return null;
})
.define('view-document', (user, document) => {
return document.published && user.hasPermission('documents.view');
})
.after((result, ability, user, resource) => {
// Audit logging
if (resource?.id) {
auditLog.record({
tenantId: user.tenantId,
userId: user.id,
action: ability,
resourceId: resource.id,
granted: result
});
}
return result;
})
);Time-Based Access Control (TBAC)
const gate = initialize(keeper =>
keeper
.before((ability, user) => {
const hour = new Date().getHours();
// Maintenance window - admins only
if (hour >= 2 && hour <= 4 && user.role !== 'admin') {
return false;
}
// Business hours for financial operations
if (ability.includes('financial') && (hour < 9 || hour > 17)) {
return user.hasPermission('after-hours-access');
}
return null;
})
);🛠️ Error Handling
Errors are properly propagated through the authorization chain:
const gate = initialize(keeper =>
keeper
.before(async (ability, user) => {
// If this throws, the error propagates
if (await validateUserSession(user.sessionId)) {
return true;
}
return null;
})
.define('secure-action', (user) => true)
);
try {
await gate.allows('secure-action', user);
} catch (error) {
// Handle session validation errors
}Errors propagate immediately and stop the authorization chain. Use try-catch blocks around authorization check calls when callbacks perform async operations that might throw.
🎓 Core Concepts
Think of Gates as global authorization rules and Policies as resource-specific authorization rules. Use Gates for application-wide permissions (user roles, feature flags). Use Policies for resource-specific logic (post ownership, document access).
Gate and Keeper
initialize(initializer)creates a new keeper internally, passes it into yourinitializerso you can register hooks/abilities/policies, and returns a gate instance.- Do all registrations inside the initializer chain;
initializereturns only thegateinstance.
Abilities
- Abilities are exact string keys such as
"create:post","post_view","post-edit", etc. - Define with
keeper.define("ability", (...args) => boolean | Promise<boolean>). - If an ability is not defined and no matching policy exists, the result defaults to false.
Policies
- Register with
keeper.policy("resource", PolicyClass). Abilities shaped as"resource:method"are routed to matching methods on the policy class. - Precedence: A registered policy takes precedence for
"resource:method"and"resource"abilities over a.define(...)with the same name.
Hooks
before(...args): run before the main check (global + optional policy hook).- Return
true→ allow immediately. - Return
false→ deny immediately. - Return
null | undefined→ continue to the main check.
- Return
after(result, ability?, ...args): run after the main check (global + optional policy hook).- Usually cosidered as a logging/cleanup hook, but can be used to modify the result if the result of the main check is nullish.
Execution Order
- Global Before Hooks → Execute first for all abilities
- Policy Before Hook → If routed ability matches a policy, run for policy methods
- Policy Method → If routed ability matches a policy, run the specific authorization logic for the resource (e.g.,
MessagePolicy.view()) - Ability Method → If policy method returns null or undefined, run the ability method
- Global After Hooks → Execute last for logging/cleanup for all main checks
🔄 Lifecycle Hooks
Lifecycle hooks provide powerful global control over authorization flow:
Before Hooks
before() hooks run before any specific ability check. They can:
- Grant access by returning
true(skips ability check afterwards) - Deny access by returning
false(skips ability check afterwards) - Continue by returning
nullorundefined(proceeds to ability check)
const gate = initialize(keeper =>
keeper
.before((ability, user) => {
// Global admin access
if (user.role === 'super-admin') return true;
// Reject all access if the user is suspended
if (user.isSuspended) return false;
// Continue to specific ability
return null;
})
.before(async (ability, user, resource) => {
if (ability.startsWith('admin-')) {
const hasAdminAccess = await checkAdminPermissions(user.id);
return hasAdminAccess;
}
})
.define('edit-post', (user, post) => user.id === post.authorId)
);After Hooks
after() hooks run after the ability check and can modify the final result:
const gate = initialize(keeper =>
keeper
.define('view-sensitive-data', (user) => user.clearanceLevel >= 5)
.after((result, ability, user) => {
// Log all authorization decisions
logger.log({
userId: user.id,
ability,
granted: result,
timestamp: new Date()
});
return result; // Return unchanged
})
.after((result, ability, user) => {
// Fallback for nullish result
if ((result === null || result === undefined) && user.role === 'admin') return true;
})
);Multiple before/after hooks are supported and execute in registration order. However, organizing all global authorization logic in single hooks is recommended for maintainability.
NOTE: If any before hook returns a non-nullish value (
trueorfalse), execution stops there and subsequent before hooks and ability checking are skipped. Only the available after hooks will run after that.
🎯 Ability Naming Conventions
The library supports multiple naming conventions for abilities:
// ✅ Supported formats
"create-post" // kebab-case
"create_post" // snake_case
"create:post" // colon:separator (used for policies)
"create|post" // pipe|separator
"create.post" // dot.notation
"create post" // space separator
"createPost" // camelCase
"CreatePost" // PascalCase
"create" // any alphabetic string
// 🎯 Policy method resolution
"message:view" // Calls MessagePolicy.view()
"message:update" // Calls MessagePolicy.update()
"message:delete" // Calls MessagePolicy.delete()
// 🔮 Wildcard support (planned)
"*:post" // Any action on post resource
"message:*" // Any action on message resource
"*" // Any action on any resource📚 API Reference
initialize(initializer)
function initialize<T>(
initializer: (keeper: GateKeeperContract) => GateKeeperContract<T>
): Gate<T>Returns a configured Gate. All registrations must be done inside initializer.
GateKeeperContract Methods
// Lifecycle hooks
keeper.before(callback: BeforeCallback): GateKeeperContract
keeper.after(callback: AfterCallback): GateKeeperContract
// Ability definition
keeper.define(ability: string, callback: AbilityCallback): GateKeeperContract
// Policy registration
keeper.policy(resource: string, instance: Policy): GateKeeperContractGate Methods
// Single checks
gate.allows(ability: string, ...args: unknown[]): Promise<boolean>
gate.denies(ability: string, ...args: unknown[]): Promise<boolean>
// Batch checks - Object mapper
gate.all(mapper: AbilityMapper): Promise<boolean>
gate.any(mapper: AbilityMapper): Promise<boolean>
gate.none(mapper: AbilityMapper): Promise<boolean>
// Batch checks - Tuple arrays
gate.allOf(tuples: AbilityTuple[]): Promise<boolean>
gate.anyOf(tuples: AbilityTuple[]): Promise<boolean>
gate.noneOf(tuples: AbilityTuple[]): Promise<boolean>Callback Types
// Ability definition
type AbilityCallback = (...args: readonly unknown[]) => boolean | null | undefined | Promise<boolean | null | undefined>
// Lifecycle hooks
type BeforeCallback = (ability: string, ...args: readonly unknown[]) => boolean | null | undefined | Promise<boolean | null | undefined>
type AfterCallback = (result: boolean | null | undefined, ability: string, ...args: readonly unknown[]) => boolean | null | undefined | Promise<boolean | null | undefined>Policy base class
abstract class Policy {
before?(ability?: string, ...args: unknown[]): boolean | null | Promise<boolean | null>
// Your methods here will be auto-mapped to abilities
// e.g., view() → "resource:view"
}🗺️ Roadmap
The following features are planned for future releases:
- Policy Auto-Discovery - Automatic policy registration via directory scanning
- User Resolution - Automatic user injection from request context
- Framework Adapters - Express, Fastify, and Nest.js middleware integration
- Nest.js Decorators -
@Can(),@Cannot()route decorators - Wildcard Support -
message:*,*:post,*ability patterns
📁 Project Structure
src/
├── core/
│ ├── ability-evaluator.ts # Ability execution engine
│ ├── gate-keeper.ts # Ability definition and registration
│ ├── gate.ts # Authorization checking logic
│ ├── policy.ts # Abstract Policy base class
│ └── index.ts # Core exports
├── interfaces/
│ ├── ability-evaluator-config.interface.ts # Evaluator configuration
│ ├── gate-keeper.interface.ts # GateKeeper contract
│ ├── policy-ability-parts.interface.ts # Policy parsing interface
│ └── index.ts # Interface exports
├── types/
│ ├── ability-registry.type.ts # Registry and argument types
│ ├── ability.type.ts # Type-safe ability validation
│ ├── auth-response.type.ts # Response type definitions
│ ├── callback.type.ts # Callback function types
│ ├── policy.type.ts # Policy method type utilities
│ └── index.ts # Type exports
├── utils/
│ ├── array.utils.ts # Array utility functions
│ ├── object.utils.ts # Object utility functions
│ ├── type.utils.ts # Type checking utilities
│ └── index.ts # Utility exports
└── index.ts # Main entry point📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
