you-cant
v1.0.2
Published
A lightweight TypeScript policy engine for composing authorization rules with clear, reusable primitives.
Readme
you-cant
A lightweight TypeScript policy engine for composing authorization rules with clear, reusable primitives.
Installation
npm install you-cantOverview
you-cant helps you define authorization rules as composable policies instead of scattered conditionals. It supports:
- Permission checks
- Predicate-based conditions
- Logical composition with
all,any, andnot - Named policies for clearer failure messages
- Boolean, detailed, throwing, and
Result-based authorization flows
It is intentionally small. If you prefer not to add another dependency, you can also copy the library into your codebase and adapt it to your needs.
Advantages over traditional checks
With traditional authorization, rules often live inline inside handlers or services:
function canUpdateTodo(user: User, todo: Todo): boolean {
if (user.permissions.has('admin.update_todo')) return true
if (
user.permissions.has('tenant.update_todo') &&
user.tenantId === todo.tenantId &&
todo.visibility === 'team'
) {
return true
}
if (
user.permissions.has('member.update_todo') &&
user.tenantId === todo.tenantId &&
todo.visibility === 'private' &&
todo.createdByUserId === user.id
) {
return true
}
return false
}That works, but as rules grow it becomes harder to:
- Reuse the same logic across multiple endpoints
- Explain why access was denied
- Review authorization separately from business logic
- Keep checks consistent everywhere they are needed
- Read the intent quickly when the logic is buried in nested
ifstatements
With you-cant, the same logic becomes a reusable policy that can be evaluated anywhere:
const UpdateTodoPolicy = any<TodoPermissions, UpdateTodoContext>(
named('admin can update todos in any tenant', permission('admin.update_todo')),
named(
'tenant users can update team todos in their own tenant',
all(
permission('tenant.update_todo'),
predicate('todo belongs to actor tenant', (ctx) => ctx.actorTenantId === ctx.todo.tenantId),
predicate('todo is team visible', (ctx) => ctx.todo.visibility === 'team')
)
),
named(
'members can update their own private todos in their own tenant',
all(
permission('member.update_todo'),
predicate('todo belongs to actor tenant', (ctx) => ctx.actorTenantId === ctx.todo.tenantId),
predicate('todo is private', (ctx) => ctx.todo.visibility === 'private'),
predicate('todo was created by actor', (ctx) => ctx.todo.createdByUserId === ctx.actorUserId)
)
)
)The library approach separates policy definition from application flow, makes the rules easier to reuse, and gives you more consistent authorization behavior across the codebase. It is also easier to read because the policy names and predicates describe intent directly, instead of forcing the reader to interpret nested if statements that do not say much on their own.
Future scope
A possible future direction is policy serialization. This is not properly supported today because predicates are arbitrary functions and cannot be serialized in a reliable general-purpose way.
That said, serialization could work well for policy shapes built from boolean permission structures, such as permission, all, any, and not, where the rules can be represented as plain data instead of executable code.
Comparison with CASL, Casbin, and other RBAC libraries
Libraries such as CASL, Casbin, and more complete RBAC or ABAC systems are often a better fit when you need a broader authorization platform with features like policy storage, adapters, cross-service enforcement, or standardized models.
you-cant takes a different approach. It is useful when you want:
- A very small library you can understand quickly
- Policies written directly in TypeScript
- Full control over your authorization logic without adopting a larger framework
- Simple composition of permissions and predicates inside application code
- Something small enough that you could even copy into your own codebase
Compared with those larger tools, the tradeoff is that you-cant is intentionally minimal. It does not try to be a full authorization system, a centralized policy engine, or a persistence-backed RBAC platform. It is closer to a tiny policy composition library for code-first authorization.
Usage
import {
all,
any,
authorize,
authorizeDetailed,
named,
permission,
predicate,
type PolicyContext,
} from 'you-cant'
type Todo = {
tenantId: string
createdByUserId: string
visibility: 'private' | 'team'
}
type TodoPermissions =
| 'admin.create_todo'
| 'tenant.create_todo'
| 'member.create_todo'
| 'admin.update_todo'
| 'tenant.update_todo'
| 'member.update_todo'
type CreateTodoContext = {
actorTenantId?: string
actorUserId: string
todoTenantId: string
visibility: 'private' | 'team'
}
const CreateTodoPolicy = any<TodoPermissions, CreateTodoContext>(
named(
'admin can create todos in any tenant',
permission('admin.create_todo')
),
named(
'tenant users can create todos in their own tenant',
all(
permission('tenant.create_todo'),
predicate(
'todo belongs to actor tenant',
(ctx) => !!ctx.actorTenantId && ctx.actorTenantId === ctx.todoTenantId
)
)
),
named(
'members can create private todos in their own tenant',
all(
permission('member.create_todo'),
predicate('todo belongs to actor tenant', (ctx) => !!ctx.actorTenantId && ctx.actorTenantId === ctx.todoTenantId),
predicate('visibility is private', (ctx) => ctx.visibility === 'private'),
)
)
)
type UpdateTodoContext = {
actorTenantId?: string
actorUserId: string
todo: Todo
}
const UpdateTodoPolicy = any<TodoPermissions, UpdateTodoContext>(
named(
'admin can update todos in any tenant',
permission('admin.update_todo')
),
named(
'tenant users can update team todos in their own tenant',
all(
permission('tenant.update_todo'),
predicate(
'todo belongs to actor tenant',
(ctx) => !!ctx.actorTenantId && ctx.actorTenantId === ctx.todo.tenantId
),
predicate('todo is team visible', (ctx) => ctx.todo.visibility === 'team')
)
),
named(
'members can update their own private todos in their own tenant',
all(
permission('member.update_todo'),
predicate(
'todo belongs to actor tenant',
(ctx) => !!ctx.actorTenantId && ctx.actorTenantId === ctx.todo.tenantId
),
predicate('todo is private', (ctx) => ctx.todo.visibility === 'private'),
predicate('todo was created by actor', (ctx) => ctx.todo.createdByUserId === ctx.actorUserId)
)
)
)
const createAllowedContext: PolicyContext<TodoPermissions, CreateTodoContext> = {
permissions: new Set(['tenant.create_todo']),
actorTenantId: 'tenant_123',
actorUserId: 'user_1',
todoTenantId: 'tenant_123',
visibility: 'team',
}
const createDeniedContext: PolicyContext<TodoPermissions, CreateTodoContext> = {
permissions: new Set(['member.create_todo']),
actorTenantId: 'tenant_123',
actorUserId: 'user_2',
todoTenantId: 'tenant_123',
visibility: 'team',
}
const updateAllowedContext: PolicyContext<TodoPermissions, UpdateTodoContext> = {
permissions: new Set(['member.update_todo']),
actorTenantId: 'tenant_123',
actorUserId: 'user_7',
todo: {
tenantId: 'tenant_123',
createdByUserId: 'user_7',
visibility: 'private',
},
}
const createAllowed = authorize(CreateTodoPolicy, createAllowedContext)
// true
const createDenied = authorizeDetailed(CreateTodoPolicy, createDeniedContext)
// { ok: false, reason: '...' }
const updateAllowed = authorize(UpdateTodoPolicy, updateAllowedContext)
// trueAPI
Policy builders
permission(name, label?)predicate(label, check)all(...children)any(...children)not(child)named(label, policy)must(requiredPermissions, ...extraPolicies)and(...permissions)or(...permissions)
Authorization helpers
authorize(policy, context)returnsbooleanauthorizeDetailed(policy, context)returns{ ok: true } | { ok: false; reason: string }authorizeWithResult(policy, context)returnsResult<void, string>assertAuthorized(policy, context, message?)throws when authorization fails
Result helper
The package also exports a small Result utility with:
Result.ok(value)Result.fail(error)mapmapErrorandThenunwrapOrThrowsafelyUnwrap
Build
npm run buildLicense
MIT
