Skip to main content

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.


Environment extraction

Hono's default environment extractor reads:

  • cf-connecting-ip (Cloudflare) or x-forwarded-for for the IP address
  • user-agent for the user agent string
  • Date.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 RedisAdapter against 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

OptionTypeDefaultDescription
getUserId(c) -> string or nullContext userId or x-user-id headerExtract the subject ID
getAction(c) -> stringHTTP method mapMap the request to an action
getResource(c) -> ResourceInfer from URL pathMap the request to a resource
getEnvironment(c) -> EnvironmentCF/forwarded IP + user agent + timestampExtract environment context
getScope(c) -> string or undefinedundefinedExtract scope from the request
onDenied(c) -> Response403 JSONCustom denial response
onError(err, c) -> Response500 JSONCustom error handler