hono
Hono middleware and per-route guard for duck-iam. Edge-friendly with cf-connecting-ip detection and zero hard dependencies.
Install
import { accessMiddleware, guard } from '@gentleduck/iam/server/hono'import { accessMiddleware, guard } from '@gentleduck/iam/server/hono'Edge-friendly. Works on Bun, Cloudflare Workers, Deno Deploy, Vercel Edge — anywhere Hono runs.
Global middleware
Apply access control to all routes under a path pattern. If no user ID is found, the middleware returns 401 immediately.
import { accessMiddleware } from '@gentleduck/iam/server/hono'
app.use(
'/api/*',
accessMiddleware(engine, {
getUserId: (c) => (c.get('userId') as string) ?? c.req.header('x-user-id'),
getScope: (c) => c.req.header('x-org-id'),
onDenied: (c) => c.json({ error: 'Forbidden' }, 403),
onError: (err, c) => c.json({ error: 'Internal server error' }, 500),
}),
)import { accessMiddleware } from '@gentleduck/iam/server/hono'
app.use(
'/api/*',
accessMiddleware(engine, {
getUserId: (c) => (c.get('userId') as string) ?? c.req.header('x-user-id'),
getScope: (c) => c.req.header('x-org-id'),
onDenied: (c) => c.json({ error: 'Forbidden' }, 403),
onError: (err, c) => c.json({ error: 'Internal server error' }, 500),
}),
)Per-route guard
Guard individual Hono routes with fixed action and resource types.
import { guard } from '@gentleduck/iam/server/hono'
app.delete('/posts/:id', guard(engine, 'delete', 'post'), async (c) => {
// Only reached if the user can delete posts
return c.json({ deleted: true })
})
app.post('/admin/users', guard(engine, 'manage', 'user', { scope: 'admin' }), async (c) => {
return c.json({ created: true })
})import { guard } from '@gentleduck/iam/server/hono'
app.delete('/posts/:id', guard(engine, 'delete', 'post'), async (c) => {
// Only reached if the user can delete posts
return c.json({ deleted: true })
})
app.post('/admin/users', guard(engine, 'manage', 'user', { scope: 'admin' }), async (c) => {
return c.json({ created: true })
})The guard automatically reads c.req.param('id') as the resource ID when available.
Like the Express guard, the Hono guard() helper assumes the route param is named id.
For nested resources or different param names, prefer accessMiddleware() with a custom
getResource function or a manual engine.can() call in the handler.
Environment extraction
Hono's default environment extractor reads:
cf-connecting-ip(Cloudflare) orx-forwarded-forfor the IP addressuser-agentfor the user agent stringDate.now()for the timestamp
Override with getEnvironment to add custom fields:
accessMiddleware(engine, {
getEnvironment: (c) => ({
ip: c.req.header('cf-connecting-ip') ?? c.req.header('x-real-ip'),
userAgent: c.req.header('user-agent'),
region: c.req.header('cf-ipcountry'),
timestamp: Date.now(),
}),
})accessMiddleware(engine, {
getEnvironment: (c) => ({
ip: c.req.header('cf-connecting-ip') ?? c.req.header('x-real-ip'),
userAgent: c.req.header('user-agent'),
region: c.req.header('cf-ipcountry'),
timestamp: Date.now(),
}),
})Edge runtime notes
The Hono integration imports zero Node-specific APIs. It works in:
- Cloudflare Workers — pair with
RedisAdapteragainst Upstash Redis or a KV adapter - Deno Deploy — works with deno-friendly adapters
- Bun — full Node compatibility plus Hono's edge optimizations
- Vercel Edge Functions — bundle the adapter as part of the Edge build
For database-backed adapters at the edge, ensure your driver supports the runtime (Neon serverless for Postgres, planetscale for MySQL, etc.).
Options reference
| Option | Type | Default | Description |
|---|---|---|---|
getUserId | (c) -> string or null | Context userId or x-user-id header | Extract the subject ID |
getAction | (c) -> string | HTTP method map | Map the request to an action |
getResource | (c) -> Resource | Infer from URL path | Map the request to a resource |
getEnvironment | (c) -> Environment | CF/forwarded IP + user agent + timestamp | Extract environment context |
getScope | (c) -> string or undefined | undefined | Extract scope from the request |
onDenied | (c) -> Response | 403 JSON | Custom denial response |
onError | (err, c) -> Response | 500 JSON | Custom error handler |