Methods
Use orThrow, filter, and explain methods on permissions
Every permission is a callable function with helpful methods for common authorization patterns.
Direct Call - Boolean Check
The simplest way to use a permission is to call it directly:
const isAdmin = permission('isAdmin', (ctx) => ctx.user.role === 'admin');
// Returns Promise<boolean>
if (await isAdmin(ctx)) {
console.log('User is an admin');
}
// With resource
const canEdit = or(isPostOwner, isAdmin);
const post = await db.getPost('123');
if (await canEdit(ctx, post)) {
await db.updatePost(post, updates);
}
.orThrow() - Require Permission
Throw an error if the permission is denied:
// Throws ForbiddenError if denied
await canEditPost.orThrow(ctx, post);
await db.updatePost(post, updates);
// With custom error message
await canEditPost.orThrow(ctx, post, 'You cannot edit this post');
// With custom Error instance
await canEditPost.orThrow(ctx, post, new ForbiddenError('Access denied'));
// With Error factory
await canEditPost.orThrow(ctx, post, () => new CustomError('Not allowed'));
Error Types
granter provides built-in error types:
import { ForbiddenError, UnauthorizedError } from 'granter';
try {
await canEdit.orThrow(ctx, post);
} catch (error) {
if (error instanceof ForbiddenError) {
// Handle 403 Forbidden
return res.status(403).json({ error: error.message });
}
throw error;
}
Use Cases
Perfect for:
- REST APIs: Protect endpoints
- GraphQL Resolvers: Guard field resolution
- Server Actions: Next.js/Remix form handlers
- RPC handlers: tRPC, Hono RPC
// Express.js
app.delete('/posts/:id', async (req, res) => {
const post = await getPost(req.params.id);
try {
await canDelete.orThrow(req.ctx, post);
await deletePost(post);
res.json({ success: true });
} catch (error) {
if (error instanceof ForbiddenError) {
return res.status(403).json({ error: error.message });
}
throw error;
}
});
// Next.js Server Action
async function updatePost(postId: string, updates: Partial<Post>) {
const session = await getSession();
const ctx = { user: session.user, db };
const post = await db.getPost(postId);
await canEdit.orThrow(ctx, post); // Throws if not allowed
return db.updatePost(postId, updates);
}
.filter() - Filter Arrays
Filter an array of resources to only those the user can access:
const allPosts = await db.getPosts();
// Returns only posts user can edit
const editablePosts = await canEdit.filter(ctx, allPosts);
console.log(`User can edit ${editablePosts.length} of ${allPosts.length} posts`);
How It Works
The .filter() method:
- Checks the permission for each item in parallel
- Returns a new array with only allowed items
- Preserves original array order
// Internal behavior (simplified)
async filter(ctx, resources) {
const results = await Promise.all(
resources.map(resource => this(ctx, resource))
);
return resources.filter((_, index) => results[index]);
}
Use Cases
Perfect for:
- List endpoints: Return only accessible items
- UI rendering: Show only available actions
- Batch operations: Process only allowed items
// REST API - list editable posts
app.get('/posts/editable', async (req, res) => {
const allPosts = await db.getPosts();
const editablePosts = await canEdit.filter(req.ctx, allPosts);
res.json({ posts: editablePosts });
});
// GraphQL - filter in resolver
const resolvers = {
Query: {
myEditablePosts: async (_, __, ctx) => {
const allPosts = await db.getPosts({ authorId: ctx.user.id });
return canEdit.filter(ctx, allPosts);
},
},
};
// UI - show available actions
async function PostList({ posts, ctx }) {
const deletablePosts = await canDelete.filter(ctx, posts);
const deletableIds = new Set(deletablePosts.map(p => p.id));
return posts.map(post => (
<Post key={post.id}>
{deletableIds.has(post.id) && <DeleteButton />}
</Post>
));
}
Performance Note
.filter() checks all items in parallel. For large arrays, consider pagination or more selective queries.
.explain() - Debug Permissions
Understand why a permission passed or failed:
const canEdit = and(
isAuthenticated,
not(isPostLocked),
or(isPostOwner, isAdmin)
);
const explanation = await canEdit.explain(ctx, post);
console.log(JSON.stringify(explanation, null, 2));
Output:
{
"name": "(isAuthenticated AND (NOT isPostLocked) AND (isPostOwner OR isAdmin))",
"value": false,
"duration": 23.45,
"children": [
{
"name": "isAuthenticated",
"value": true,
"duration": 0.12
},
{
"name": "(NOT isPostLocked)",
"value": true,
"duration": 0.08,
"operator": "NOT",
"children": [
{ "name": "isPostLocked", "value": false, "duration": 0.06 }
]
},
{
"name": "(isPostOwner OR isAdmin)",
"value": false,
"duration": 15.32,
"operator": "OR",
"children": [
{ "name": "isPostOwner", "value": false, "duration": 8.21 },
{ "name": "isAdmin", "value": false, "duration": 7.11 }
]
}
]
}
Explanation Structure
type ExplanationResult = {
name: string; // Permission name
value: boolean; // Result (true/false)
duration: number; // Time in milliseconds
operator?: 'OR' | 'AND' | 'NOT'; // Operator type
children?: ExplanationResult[]; // Nested permissions
};
Use Cases
Perfect for:
- Debugging: Understand complex permission failures
- Development: Verify permission logic
- Support: Help users understand why they can't access something
- Auditing: Log permission checks for compliance
// Development debugging
if (process.env.NODE_ENV === 'development') {
const explanation = await canEdit.explain(ctx, post);
console.log('Permission check:', explanation);
}
// User-facing explanation
try {
await canEdit.orThrow(ctx, post);
} catch (error) {
const explanation = await canEdit.explain(ctx, post);
// Show user why they can't edit
const reasons = [];
if (!explanation.children?.find(c => c.name === 'isAuthenticated')?.value) {
reasons.push('You must be logged in');
}
if (!explanation.children?.find(c => c.name.includes('isPostOwner'))?.value) {
reasons.push('You must be the post owner');
}
return { error: 'Cannot edit post', reasons };
}
// Audit logging
const explanation = await canDelete.explain(ctx, post);
await auditLog.create({
userId: ctx.user.id,
action: 'delete_post',
postId: post.id,
allowed: explanation.value,
reasons: explanation,
});
Performance Impact
.explain() runs all permission checks, even those that would normally short-circuit. Only use it for debugging, development, or auditing - not in hot paths.
Method Comparison
| Method | Returns | Throws | Use Case |
|---|---|---|---|
| Direct call | Promise<boolean> | ❌ No | Conditional logic |
.orThrow() | Promise<void> | ✅ Yes | Protect operations |
.filter() | Promise<T[]> | ❌ No | Filter arrays |
.explain() | Promise<ExplanationResult> | ❌ No | Debugging |
Next Steps
- withContext - Simplify permission calls
- Debugging - Deep dive into
.explain() - Express Example - See methods in a real API