Skip to content

seeden/granter

Repository files navigation

granter

Composable, type-safe authorization for TypeScript

npm version License: MIT

📚 Read the full documentation →

Why granter?

Composable - Build complex permissions from simple rules
🔒 Type-safe - Full TypeScript inference with generic contexts
Async-first - Works seamlessly with databases, APIs, and DataLoader
🔧 Framework-agnostic - Works with Express, Hono, Next.js, GraphQL, and more
🪶 Zero dependencies - Lightweight and performant

Quick Example

import { permission, or } from 'granter';

// Define permissions
const isAdmin = permission('isAdmin', (ctx) => ctx.user.role === 'admin');

const isPostOwner = permission('isPostOwner', (ctx, post) => post.authorId === ctx.user.id);

// Compose permissions
const canEditPost = or(isPostOwner, isAdmin);

// Use them - permissions are callable!
if (await canEditPost(ctx, post)) {
  await updatePost(post);
}

// Require permission (throws if denied)
await canEditPost.orThrow(ctx, post);

// Filter arrays
const editablePosts = await canEditPost.filter(ctx, allPosts);

// Debug permission checks
const explanation = await canEditPost.explain(ctx, post);

Installation

npm install granter

Documentation

Visit seeden.github.io/granter for the complete documentation:

Key Features

Composable Operators

import { and, or, not } from 'granter';

// Combine with OR (any must pass)
const canEdit = or(isPostOwner, isAdmin, isModerator);

// Combine with AND (all must pass)
const canPublish = and(isAuthenticated, isVerified, isPostOwner);

// Negate permissions
const canComment = and(isAuthenticated, not(isBanned));

Powerful Methods

// Check permission (returns boolean)
if (await canEdit(ctx, post)) {
  /* ... */
}

// Require permission (throws if denied)
await canEdit.orThrow(ctx, post);

// Filter arrays to allowed items
const editable = await canEdit.filter(ctx, allPosts);

// Debug permission checks
const explanation = await canEdit.explain(ctx, post);

Simplify with withContext()

import { withContext } from 'granter';

const abilities = withContext(ctx, {
  canEditPost,
  canDeletePost,
});

// No need to pass ctx anymore!
if (await abilities.canEditPost(post)) {
  await updatePost(post);
}

Framework Examples

granter works with any TypeScript project. See the documentation for complete examples with:

  • Express.js - REST API with middleware
  • Next.js - Server Actions and App Router
  • GraphQL - Apollo Server with DataLoader
  • React - Context and hooks patterns

Authentication Integration

granter is authorization-only and works with any authentication library:

See the Authentication Integration guide for complete examples.

TypeScript Support

granter is built with TypeScript and provides full type inference:

type AppContext = {
  user: { id: string; role: string };
  db: Database;
};

type Post = {
  id: string;
  authorId: string;
};

const canEdit = or(isPostOwner, isAdmin);

// ✅ Type-safe: ctx and post are fully typed
await canEdit(ctx, post);

// ❌ TypeScript error: missing resource
await canEdit(ctx);

Testing

Permissions are pure functions, making them easy to test:

import { describe, it, expect } from 'vitest';

describe('canEditPost', () => {
  it('allows post owner', async () => {
    const ctx = { user: { id: '1', role: 'user' }, db };
    const post = { id: '123', authorId: '1' };

    expect(await canEditPost(ctx, post)).toBe(true);
  });

  it('allows admin', async () => {
    const ctx = { user: { id: '2', role: 'admin' }, db };
    const post = { id: '123', authorId: '1' };

    expect(await canEditPost(ctx, post)).toBe(true);
  });

  it('denies other users', async () => {
    const ctx = { user: { id: '3', role: 'user' }, db };
    const post = { id: '123', authorId: '1' };

    expect(await canEditPost(ctx, post)).toBe(false);
  });
});

Advanced Features

Parallel Operators

Use orParallel() and andParallel() for DataLoader batching:

import { orParallel, andParallel } from 'granter';

// Run all checks in parallel (no short-circuit)
const canEdit = orParallel(isPostOwner, isAdmin, isModerator);

Learn more about parallel execution →

Debug with .explain()

Understand why permissions passed or failed:

const explanation = await canEdit.explain(ctx, post);
console.log(JSON.stringify(explanation, null, 2));
// {
//   "name": "(isPostOwner OR isAdmin)",
//   "value": false,
//   "duration": 15.23,
//   "children": [
//     { "name": "isPostOwner", "value": false, "duration": 8.12 },
//     { "name": "isAdmin", "value": false, "duration": 7.11 }
//   ]
// }

Learn more about debugging →

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT © seeden


📚 View Full Documentation | GitHub | npm

About

Composable, type-safe authorization for TypeScript. ⭐️ Star to support my work

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published