Getting Started
Installation
sh
npm install @falcondev-oss/caps
sh
yarn add @falcondev-oss/caps
sh
pnpm add @falcondev-oss/caps
sh
bun add @falcondev-oss/caps
Basic Concepts
This library is built around several key concepts:
- Actor: The entity performing an action (user, service, etc.)
- Subject: The target of an action (document, user profile, etc.)
- Capability: A specific permission or action that can be performed
- Generator Functions: Define capabilities using yield for permissions and return for final results
Simple Example
Let's start with a basic example that demonstrates core concepts:
ts
import { createActor } from '@falcondev-oss/caps'
// Define what an actor looks like
type User = {
role: 'user' | 'admin'
}
// Create an actor definition
const useActor = createActor<User>().build((cap) => ({
// Define a simple capability
documents: cap.define(function* ({ actor }) {
// All users can read documents
yield ['read']
// Only admins can create and delete
if (actor.role === 'admin') {
yield ['create', 'delete']
}
return []
}),
}))
// Use the capabilities
const user = { role: 'user' }
const admin = { role: 'admin' }
const userCaps = useActor(user)
const adminCaps = useActor(admin)
// Check capabilities
userCaps.documents.can('read').check() // true
userCaps.documents.can('delete').check() // false
adminCaps.documents.can('read').check() // true
adminCaps.documents.can('delete').check() // true
// List all available capabilities
userCaps.documents.list() // ['read']
adminCaps.documents.list() // ['read', 'create', 'delete']
Subject-Based Permissions
Often you need to check permissions on specific objects (subjects):
ts
type User = {
id: string
role: 'user' | 'admin'
}
type Document = {
id: string
ownerId: string
isPublic: boolean
}
const useActor = createActor<User>().build((cap) => ({
document: cap.subject<Document>().define(function* ({ actor, subject }) {
// Everyone can read public documents
if (subject.isPublic) {
yield ['read']
}
// Owners can read and edit their own documents
if (actor.id === subject.ownerId) {
yield ['read', 'edit', 'delete']
}
// Admins can do everything
if (actor.role === 'admin') {
yield ['read', 'edit', 'delete', 'publish']
}
return []
}),
}))
const user = { id: '1', role: 'user' }
const caps = useActor(user)
const publicDoc = { id: 'doc1', ownerId: '2', isPublic: true }
const ownDoc = { id: 'doc2', ownerId: '1', isPublic: false }
const privateDoc = { id: 'doc3', ownerId: '2', isPublic: false }
// Check permissions on specific documents
caps.document.subject(publicDoc).can('read').check() // true
caps.document.subject(ownDoc).can('edit').check() // true
caps.document.subject(privateDoc).can('read').check() // false
Arguments and Conditional Logic
You can pass arguments to capability checks for more complex logic:
ts
import { arg } from '@falcondev-oss/caps'
type User = {
id: string
role: 'user' | 'moderator' | 'admin'
}
const useActor = createActor<User>().build((cap) => ({
user: cap.subject<User>().define(
function* ({ actor, subject, args }) {
// Everyone can read users
yield ['read']
// Users can update themselves
if (actor.id === subject.id) {
yield ['update']
}
// Role-based permissions with arguments
if (actor.role === 'admin') {
yield ['ban', 'promote']
// Admins can delete, but only with confirmation
if (args.delete?.confirmed) {
yield ['delete']
}
}
if (
actor.role === 'moderator' && // Moderators can only promote to certain roles
(args.promote?.targetRole === 'user' || args.promote?.targetRole === 'moderator')
) {
yield ['promote']
}
return []
},
// Define argument types
{
delete: arg<{ confirmed: boolean }>(),
promote: arg<{ targetRole: 'user' | 'moderator' | 'admin' }>(),
},
),
}))
const admin = { id: '1', role: 'admin' }
const moderator = { id: '2', role: 'moderator' }
const targetUser = { id: '3', role: 'user' }
const adminCaps = useActor(admin)
const modCaps = useActor(moderator)
// Using arguments in capability checks
adminCaps.user.subject(targetUser).can('delete', { confirmed: true }).check() // true
adminCaps.user.subject(targetUser).can('delete', { confirmed: false }).check() // false
modCaps.user.subject(targetUser).can('promote', { targetRole: 'moderator' }).check() // true
modCaps.user.subject(targetUser).can('promote', { targetRole: 'admin' }).check() // false
Bulk Operations
Act on multiple subjects at once:
ts
type User = {
id: string
role: 'user' | 'moderator' | 'admin'
department: string
}
const useActor = createActor<User>().build((cap) => ({
user: cap.subject<User>().define(function* ({ actor, subject }) {
yield ['read']
// Can edit users in same department
if (actor.department === subject.department && actor.role === 'moderator') {
yield ['edit']
}
if (actor.role === 'admin') {
yield ['edit', 'delete']
}
return []
}),
}))
const admin = { id: '1', role: 'admin', department: 'IT' }
const caps = useActor(admin)
const users = [
{ id: '2', role: 'user', department: 'IT' },
{ id: '3', role: 'user', department: 'HR' },
{ id: '4', role: 'moderator', department: 'IT' },
]
// Filter users that can be edited
caps.user.subjects(users).filter(['edit']) // all users (admin can edit everyone)
// Check if admin can edit all users
caps.user.subjects(users).canEvery('edit').check() // true
// Check if admin can delete any user
caps.user.subjects(users).canSome('delete').check() // true
Advanced Example: User Management System
Here's a comprehensive example showing a realistic user management system:
ts
import { arg, createActor, MissingCapabilityError } from '@falcondev-oss/caps'
type Role = 'user' | 'moderator' | 'admin'
type User = {
userId: string
role: Role
isBanned?: boolean
}
const useActor = createActor<User>().build((cap) => ({
user: cap.subject<User>().define(
function* ({ actor, subject, args }) {
// Banned users can't do anything
if (actor.isBanned) return []
// Everyone can read users
yield ['read']
// Users can update and delete themselves
if (actor.userId === subject.userId) {
yield ['update', 'delete']
}
// Admin permissions
if (actor.role === 'admin') {
yield ['create', 'update', 'set_role']
// Admins can delete, but only with delay confirmation
if (args.delete?.delayed) {
yield ['delete']
}
}
// Moderator permissions
if (
actor.role === 'moderator' &&
subject.role !== 'admin' && // Can't affect admins
(args.set_role?.role === 'user' || args.set_role?.role === 'moderator')
) {
yield ['set_role']
}
return []
},
{
set_role: arg<{ role: Role }>(),
delete: arg<{ delayed: boolean }>(),
},
),
}))
// Example usage
const regularUser = { userId: '1', role: 'user' as const }
const moderator = { userId: '2', role: 'moderator' as const }
const admin = { userId: '3', role: 'admin' as const }
const targetUser = { userId: '4', role: 'user' as const }
// Regular user capabilities
const userCaps = useActor(regularUser)
userCaps.user.subject(targetUser).list({}) // ['read']
// Admin capabilities
const adminCaps = useActor(admin)
adminCaps.user.subject(targetUser).list({
delete: { delayed: true },
set_role: { role: 'moderator' },
}) // ['read', 'create', 'update', 'set_role', 'delete']
// Error handling
try {
userCaps.user.subject(targetUser).can('delete', { delayed: true }).throw()
} catch (err) {
err instanceof MissingCapabilityError // true
err.message // "Missing capability: 'delete'"
}
// Bulk operations with complex filtering
const allUsers = [regularUser, moderator, admin, targetUser]
adminCaps.user.subjects(allUsers).filter(['delete'], { delete: { delayed: true } }) // all users
Working with Modes
For complex scenarios where subjects can have different "modes" or states:
ts
import { mode, type Modes } from '@falcondev-oss/caps'
type DocumentModes = Modes<{
draft: { content: string; authorId: string }
published: { content: string; authorId: string; publishedAt: Date }
archived: { content: string; authorId: string; archivedAt: Date }
}>
const useActor = createActor<User>().build((cap) => ({
document: cap.subject<DocumentModes>().define(function* ({ actor, subject }) {
switch (subject.__mode) {
case 'draft': {
if (subject.authorId === actor.id) {
yield ['read', 'edit', 'publish', 'delete']
}
break
}
case 'published': {
yield ['read'] // Everyone can read published docs
if (subject.authorId === actor.id) {
yield ['edit', 'archive']
}
break
}
case 'archived': {
if (actor.role === 'admin') {
yield ['read', 'restore', 'delete']
}
break
}
}
return []
}),
}))
// Usage with modes
const author = { id: '1', role: 'user' }
const caps = useActor(author)
const draftDoc = mode('draft', { content: 'Hello', authorId: '1' })
const publishedDoc = mode('published', {
content: 'Hello World',
authorId: '1',
publishedAt: new Date(),
})
caps.document.subject(draftDoc).can('edit').check() // true
caps.document.subject(publishedDoc).can('publish').check() // false