npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@api-policy/hono

v2.0.0

Published

Hono.js adapter for API Policy. Auth middleware, permissions, routing.

Readme

@api-policy/hono

Hono adapter for @api-policy/server. Drop-in auth middleware, route factory, and permission enforcement for Hono applications.

npm install @api-policy/hono @api-policy/server hono jose

Validation schemas use Standard Schema — bring your own library (Zod v4, Valibot, ArkType), or use the built-in slugId with zero dependencies.


Setup

import { Hono } from 'hono'
import {
  createPolicyPlugin,
  defineRoute,
  definePublicRoute,
  createRouteGroup,
  combineRoutes,
  requestContextMiddleware,
  BOUNDARY,
  PERM,
} from '@api-policy/hono'
import { ok } from 'ts-micro-result'

// 1. Create plugin (wires up auth + permission engines)
const policy = createPolicyPlugin({
  auth: {
    jwt: { secret: process.env.JWT_SECRET! },
  },
})

// 2. Define routes
const listPosts = defineRoute(policy, {
  method: 'GET',
  path: '/',
  auth: true,
  permission: { resource: 'post', action: PERM.READ, boundary: BOUNDARY.TENANT },
  handler: async ({ user, query }) => {
    const posts = await db.posts.list(user!.tenantId)
    return ok(posts)
  },
})

const health = definePublicRoute(policy, {
  method: 'GET',
  path: '/health',
  handler: async () => ok({ status: 'ok' }),
})

// 3. Group and mount
const app = new Hono()
app.use('*', requestContextMiddleware)
app.route('/api/v1', createRouteGroup('/posts', [listPosts]))
app.route('/api/v1', health)

createPolicyPlugin

Creates the auth and permission engines used by all routes.

import { createPolicyPlugin } from '@api-policy/hono'
import { createGatewayJwtAuthMethod, createDefaultJwtVerify } from '@api-policy/server'

const policy = createPolicyPlugin({
  auth: {
    // Default methods when route has auth: true
    defaultMethods: ['jwt', 'apiKey'],

    // JWT — symmetric (HS256) or asymmetric (RS256, ES256, EdDSA)
    jwt: { secret: process.env.JWT_SECRET! },
    // jwt: { publicKey: process.env.JWT_PUBLIC_KEY!, algorithm: 'EdDSA' },
    // jwt: { publicKey: '...', verify: myCustomVerifyFn },  // custom verify

    // API Key
    apiKey: {
      lookup: async (key) => {
        const record = await db.apiKeys.findOne({ key })
        return record ? { ownerId: record.userId, perms: record.perms, tenantId: record.tenantId } : null
      },
    },

    // Custom auth methods (e.g. Gateway JWT for BFF → backend)
    customMethods: [myCustomAuthMethod],
  },

  // Custom error → HTTP status codes (merged with system defaults)
  errorHttpMap: {
    PRODUCT_NOT_FOUND: 404,
    PRODUCT_SLUG_EXISTS: 409,
  },
})

// policy.auth         → AuthPolicyEngine
// policy.permission   → PermissionEngine
// policy.errorHttpMap → merged error→HTTP status map

Gateway JWT mode (BFF → backend)

If your service sits behind a BFF (e.g. Cloudflare Worker) that verifies the client JWT and forwards requests with a signed Gateway JWT:

import { createGatewayJwtAuthMethod, createDefaultJwtVerify } from '@api-policy/server'

const jwtConfig = { secret: process.env.JWT_SECRET! }

const policy = createPolicyPlugin({
  auth: {
    defaultMethods: ['gatewayJwt'],
    customMethods: [
      createGatewayJwtAuthMethod({
        gateway: {
          publicKeyHex: process.env.BFF_PUBLIC_KEY_HEX,  // omit for local dev
          expectedIssuer: 'bff-worker',
          expectedAudience: 'my-backend',
        },
        accessJwt: {
          config: jwtConfig,
          verifyFn: createDefaultJwtVerify(jwtConfig),
        },
      }),
    ],
  },
})

defineRoute

Builds a Hono middleware handler with the full auth pipeline attached.

import { defineRoute, BOUNDARY, PERM } from '@api-policy/hono'
import { z } from 'zod'  // or valibot, arktype — any Standard Schema library

const createPost = defineRoute(policy, {
  method: 'POST',
  path: '/',
  auth: true,                    // use default methods
  // auth: 'jwt'                 // specific method
  // auth: ['jwt', 'apiKey']     // try list in order
  permission: {
    resource: 'post',
    action: PERM.WRITE,
    boundary: BOUNDARY.TENANT,
    roles: ['editor', 'admin'],  // optional role gate (OR logic by default)
  },
  input: {
    body: z.object({ title: z.string(), content: z.string() }),
    query: z.object({ draft: z.coerce.boolean().default(false) }),
  },
  handler: async ({ user, body, query, tenantContext, traceId }) => {
    const post = await db.posts.create({ ...body, authorId: user!.id })
    return ok(post)
  },
})

Handler context (PolicyContext):

| Field | Type | Description | |-------|------|-------------| | user | UserContext \| undefined | Authenticated user (always present on auth routes) | | body | TBody | Validated request body | | query | TQuery | Validated query params | | params | TParams | Validated path params | | tenantContext | TenantContext \| undefined | Set by createTenantResolverMiddleware | | traceId | string | Request trace ID |

Middleware pipeline (in order):

1. Auth        → verify credentials, set c.var.authResult
2. Permission  → roles gate → owner bypass → bitmask → boundary
3. Validation  → validate body/query/params (Standard Schema — Zod, Valibot, ArkType, or built-in)
4. Handler     → business logic, returns Result<T>

Route metadata is attached to the returned handler:

createPost.routeMeta  // { method: 'POST', path: '/', authRequired: true }

definePublicRoute

Shorthand for routes with no auth (auth: false is forced, no permission field):

const health = definePublicRoute(policy, {
  method: 'GET',
  path: '/health',
  handler: async () => ok({ status: 'ok' }),
})

Route grouping

import { createRouteGroup, combineRoutes } from '@api-policy/hono'

// Group routes under a base path
const postRoutes    = createRouteGroup('/posts',    [listPosts, createPost, getPost, deletePost])
const productRoutes = createRouteGroup('/products', [listProducts, createProduct])

// Merge groups and mount
const app = new Hono()
app.route('/api/v1', combineRoutes(postRoutes, productRoutes))

createRouteGroup registers each route at basePath + route.path (e.g. /posts/:id). A route with path: '/' registers at basePath exactly.


Middleware

Request context

Generates a traceId from X-Request-ID / X-Correlation-ID headers, or creates a UUID. Mount once globally.

import { requestContextMiddleware, getTraceId } from '@api-policy/hono'

app.use('*', requestContextMiddleware)

// In any handler
const traceId = getTraceId(c)

Tenant resolver

Resolves tenantId from trusted sources only — never from client-supplied headers.

import { createTenantResolverMiddleware } from '@api-policy/hono'

app.use('*', createTenantResolverMiddleware({
  // Source 1 (highest priority): JWT (user.tenantId from authResult)
  // Source 2: domain lookup
  domainLookup: async (domain) => {
    return db.domains.findByHost(domain)
    // Returns: { tenantId: string, storeId?: string } | null
  },
  allowDomainFallback: true,
}))

// In handler
const { tenantId, storeId, source } = ctx.tenantContext!
// source: 'jwt' | 'domain'

For handlers that require tenantId to be non-nullable, use TenantPolicyContext:

import type { TenantPolicyContext } from '@api-policy/hono'

handler: async (ctx: TenantPolicyContext<MyBody>) => {
  ctx.user.tenantId  // string (not string | undefined)
}

Rate limiting

import { createRateLimitMiddleware, createInMemoryRateLimitCheck } from '@api-policy/hono'

// Dev/testing — in-memory sliding window
app.use('*', createRateLimitMiddleware({
  checkFn: createInMemoryRateLimitCheck(),
  defaultLimit: 1000,
}))

// Production — custom backend (Redis, etc.)
app.use('*', createRateLimitMiddleware({
  checkFn: async (tenantId, keyId, limit) => redis.slidingWindow(tenantId, keyId, limit),
  getKeyIdFn: (c) => c.var.authResult?.method,
  skipFn: (c) => c.req.path === '/health',
}))

Sets response headers automatically: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After.

Validation middleware (standalone)

validate() accepts any Standard Schema compatible library. Zod is not a required dependency.

import { validate, getValidatedParams, getValidatedQuery, slugId } from '@api-policy/hono'

// Zod v4 (optional peer dependency)
import { z } from 'zod'
app.use('/posts/:id', validate({
  params: z.object({ id: z.string() }),
  query: z.object({ page: z.coerce.number().default(1) }),
}))

// Valibot (optional peer dependency)
import * as v from 'valibot'
app.use('/posts/:id', validate({
  params: v.object({ id: v.string() }),
}))

// Built-in — zero dependency
app.use('/posts/:id', validate({ params: slugId }))

app.get('/posts/:id', (c) => {
  const params = getValidatedParams<{ id: string }>(c)
  const query  = getValidatedQuery<{ page: number }>(c)
})

Built-in (zero-dependency): slugId (lowercase, numbers, hyphens, 1–100 chars), optionalSlugId.


HTTP response

toHttpResponse is called automatically inside defineRoute. Use it directly if building custom handlers.

import { toHttpResponse, setFeatureErrorsHttpMap } from '@api-policy/hono'

// Register app-specific error codes once at startup
setFeatureErrorsHttpMap({
  PRODUCT_NOT_FOUND: 404,
  PRODUCT_SLUG_EXISTS: 409,
})

// In a custom handler
return toHttpResponse(c, result, { traceId, successStatus: 201 })

Success response:

{ "ok": true, "data": { ... }, "meta": { "traceId": "...", "params": { "timestamp": "..." } } }

Error response:

{ "ok": false, "errors": [{ "code": "PERMISSION_DENIED", "message": "..." }], "meta": { ... } }

Hono context variables

This package sets the following on Hono's ContextVariableMap:

| Key | Type | Set by | |-----|------|--------| | requestId | string | requestContextMiddleware | | correlationId | string | requestContextMiddleware | | authResult | HonoAuthResult | createAuthMiddleware | | tenantContext | TenantContext | createTenantResolverMiddleware | | validatedData | ValidatedData | validate |

c.var.authResult?.user       // UserContext
c.var.tenantContext?.tenantId
c.var.validatedData?.body

Re-exported from @api-policy/server

All commonly used exports are available directly from @api-policy/hono:

import {
  PERM, PERM_ALL, BOUNDARY, AUTH_METHOD,
  createJwtAuthMethod, createApiKeyAuthMethod,
  createGatewayJwtAuthMethod,
  createAuthPolicyEngine, createPermissionEngine,
  Forbidden, AlreadyExists, Conflict, RateLimitExceeded,
  lintRoutes, validateBuild, createPermissionRegistry,
} from '@api-policy/hono'

License

MIT