hono-rbac
v0.1.0
Published
Edge-ready Role-Based Access Control for Hono
Readme
hono-rbac
Edge-ready Role-Based Access Control for Hono
Features
- Tiny – < 5 KB core, zero Node built-ins
- Edge-safe – works on Cloudflare Workers, Deno, Bun, Node
- DX-first –
guardHelpers.role("admin")andguardHelpers.auth()one-liners - Type-safe – manifest-driven permissions
- Composable – logical algebra (
anyOf,allOf,not) - Explainable –
explain()traces every decision
Quick Start
Installation
npm install hono-rbac
# or
pnpm add hono-rbacBasic Setup
import { Hono } from 'hono'
import { createRBAC, guard, perm, anyOf, guardHelpers } from 'hono-rbac'
// 1. Define roles and permissions
const roles = {
reader: ['post.read:any'],
author: ['post.read:any', 'post.write:own'],
admin: ['*.**'], // Admin wildcard - access to everything
}
// 2. Create RBAC engine
const rbac = createRBAC({ roles })
// 3. Create Hono app
const app = new Hono()
// 4. Inject user and RBAC into context
app.use('*', async (c, next) => {
const user = { id: 'u1', roles: ['author'] }
c.set('user', user)
c.set('rbac', rbac.forUser(user))
c.set('rbacEngine', rbac)
await next()
})
// 5. Protect routes with guards
app.get('/posts', guard(perm('post.read:any')), (c) => c.json({ posts: [] }))
// Or use sugar helpers
app.get('/admin', guardHelpers.role('admin'), (c) => c.json({ message: 'Admin area' }))Guard Helpers Cheatsheet
| Helper | Usage | Description |
| ---------------------------------------------- | -------------- | ---------------------------------- |
| guardHelpers.auth() | Requires login | User must be authenticated |
| guardHelpers.role('admin') | Single role | User must have specific role |
| guardHelpers.roles.any('editor', 'admin') | Any role | User must have at least one role |
| guardHelpers.roles.all('editor', 'reviewer') | All roles | User must have all roles |
| guardHelpers.perm('billing.refund') | Permission | User must have specific permission |
| guardHelpers.can.read('post') | CRUD helper | Read permission (any or own) |
| guardHelpers.can.create('post') | CRUD helper | Create permission |
| guardHelpers.can.update.own('post', ...) | CRUD helper | Update own resource |
| guardHelpers.ownOrAdmin('post', ...) | Ownership | Owner or admin check |
Permission Format
Permissions follow the format: resource.action:scope
Examples:
post.read:any- Read any postpost.write:own- Write own postspost.**- All actions on posts*.**- Admin wildcard (everything)role.admin- Role-based check
Logical Operators
Compose complex permission checks using logical operators:
import { perm, anyOf, allOf, not } from 'hono-rbac'
// OR - at least one must match
guard(anyOf(perm('post.read:any'), perm('post.write:own')))
// AND - all must match
guard(allOf(perm('post.read'), perm('comment.read')))
// NOT - negation
guard(not(perm('post.delete')))Resource Loaders
Preload resources into context for ownership checks:
import { load } from 'hono-rbac'
// Load resource
const loadPost = load.post(async (c) => {
const id = c.req.param('id')
const post = await db.getPost(id)
c.set('post', post)
})
// Guard with ownership check
app.put(
'/posts/:id',
loadPost,
guard(
perm('post.write:own', (c, user) => {
const post = c.get('post')
return post.authorId === user.id
})
),
async (c) => {
// Handle update
}
)Or use the helper:
app.put(
'/posts/:id',
loadPost,
guardHelpers.ownOrAdmin('post', (p, u) => p.authorId === u.id),
(c) => c.text('updated!')
)Explain Decisions (Debug Mode)
Debug permission checks with detailed traces:
const rbac = createRBAC({ roles })
const userRbac = rbac.forUser(user)
const explanation = await userRbac.explain(perm('post.write:own'), c)
console.log(explanation)
// {
// allow: true,
// reason: "Access granted. User has 2 permission(s) from roles: author",
// trace: [...]
// }Type-Safe Manifest
Define your permissions with full TypeScript type safety:
import { defineManifest } from 'hono-rbac'
export const manifest = defineManifest({
resources: ['post', 'comment', 'org'] as const,
actions: ['read', 'write', 'delete', 'refund'] as const,
scopes: ['own', 'any'] as const,
})Autocompletion & validation for permission strings is generated automatically.
Complete Example
import { Hono } from 'hono'
import { createRBAC, guardHelpers, load } from 'hono-rbac'
const roles = {
author: ['post.read:any', 'post.write:own'],
admin: ['*.**'],
}
const app = new Hono()
const rbac = createRBAC({ roles })
app.use('*', (c, next) => {
c.set('user', { id: 'u1', roles: ['author'] })
c.set('rbac', rbac.forUser(c.get('user')))
c.set('rbacEngine', rbac)
return next()
})
const loadPost = load.post(async (c) => {
const post = { id: c.req.param('id'), authorId: 'u1' }
c.set('post', post)
})
app.put(
'/posts/:id',
loadPost,
guardHelpers.ownOrAdmin('post', (p, u) => p.authorId === u.id),
(c) => c.text('updated!')
)
export default appProject Structure
hono-rbac/
├── src/
│ ├── index.ts # Public API
│ ├── types.ts # TypeScript types
│ ├── permissions.ts # Permission algebra
│ ├── engine.ts # Evaluation engine
│ ├── guard.ts # RBAC engine & guard middleware
│ ├── sugar.ts # Helper functions
│ ├── loader.ts # Resource loaders
│ ├── explain.ts # Decision explanations
│ ├── manifest.ts # Type-safe manifests
│ └── store/
│ └── memory.ts # In-memory role store
├── examples/
│ ├── src/
│ │ ├── types.ts # Shared example types
│ │ ├── hono-basic/
│ │ │ └── hono-basic.ts
│ │ └── hono-own-admin/
│ │ └── hono-own-admin.ts
│ └── package.json # Example dependencies
├── dist/ # Compiled output
└── coverage/ # Test coverage reportsPhilosophy
| Principle | Description | | ------------------- | --------------------------------------------- | | DX-first | Most routes should be protectable in one line | | Functional core | Pure evaluation, no side-effects | | Explainable | Every decision traceable | | Composable | Logical algebra over flat strings | | Portable | Runs everywhere (Edge, Deno, Bun, Node) |
Test Coverage
Current coverage: 99.76% (essentially 100% of testable code)
- Statements: 99.76%
- Branches: 98.63%
- Functions: 97.91%
- Lines: 99.76%
All executable code paths are fully tested. The only uncovered files are index.ts (export file) and types.ts (type definitions), which contain no executable runtime code.
Running Examples
Basic Example
pnpm install
pnpm run devThen test with:
# As reader (can only read)
curl -H "X-User-Id: u1" -H "X-User-Role: reader" http://localhost:3000/posts
# As admin (can do everything)
curl -H "X-User-Id: u3" -H "X-User-Role: admin" http://localhost:3000/adminOwn-or-Admin Example
pnpm run dev:own-adminDevelopment
# Install dependencies
pnpm install
# Run all tests
pnpm test
# Run tests with coverage
pnpm test:coverage
# Watch mode
pnpm test:watch
# Build the project
pnpm run build
# Linting
pnpm run lint # Run all linting (types + eslint)
pnpm run lint:types # TypeScript type checking only
pnpm run lint:eslint # ESLint code quality checks
pnpm run lint:fix # Auto-fix ESLint issues
# Formatting
pnpm run format # Auto-format all code
pnpm run format:check # Check formatting without changes
# Comprehensive checks
pnpm run check # Run all checks (lint + format + test)
pnpm run fix # Auto-fix all issues (lint + format)Documentation
- Design Document - Architecture, implementation details, and contributor guide
- Release Process - Automated versioning and publishing guide
License
MIT © Zay Collier
See Also
- Hono Docs - Hono framework documentation
- Casbin - Advanced policy models
- Better Auth - Session integration
Contributing
Contributions are welcome! Please ensure:
- All tests pass (
pnpm test) - Coverage remains above 99% (
pnpm test:coverage) - Code is properly linted (
pnpm run lint) - Code is properly formatted (
pnpm run format:check) - Or simply run
pnpm run checkto verify everything at once - Use
pnpm run fixto auto-fix linting and formatting issues
Built for the Hono community
