Skip to main content

Hono adapter

WIP

Hono 4+ handlers. Runs on Bun, Node, Cloudflare Workers, Deno, and Vercel Edge.

Mount

import { Hono } from 'hono'
import {
  authHonoProviderBegin,
  authHonoSession,
  authHonoSignIn,
  authHonoSignOut,
} from '@gentleduck/auth/server/hono'
import { auth } from './lib/auth'

const app = new Hono()

app.post('/auth/signin', authHonoSignIn(auth))
app.post('/auth/signout', authHonoSignOut(auth))
app.get('/auth/session', authHonoSession(auth))
app.post('/auth/providers/:providerId/begin', (c) =>
  authHonoProviderBegin(auth, c.req.param('providerId'))(c)
)

// OAuth callbacks reuse the sign-in handler.
app.get('/auth/google/callback', authHonoSignIn(auth))
app.get('/auth/github/callback', authHonoSignIn(auth))

export default app

Resolving sessions in handlers

import { AuthError } from '@gentleduck/auth/core'

app.get('/api/me', async (c) => {
  const ctx = await auth.resolveSession(c.req.raw)
  if (!ctx) throw new AuthError('AUTH/UNAUTHENTICATED')
  return c.json(ctx.identity)
})

c.req.raw is the underlying Web-Fetch Request - duck-auth speaks that natively, so no adapter glue is needed.

Cloudflare Workers / edge

Hono on Cloudflare Workers is well-supported, but there are two gotchas for duck-auth:

  1. No Node built-ins: AuthScryptHasher uses node:crypto. On Workers, either use AuthArgon2idHasher (peer dep), or use a custom WebCrypto-based hasher.
  2. No process.env: read secrets from c.env instead:
import { createAuth } from '~/lib/createAuth'

const app = new Hono<{ Bindings: { JWT_SECRET: string; DATABASE_URL: string } }>()

app.use('*', async (c, next) => {
  c.set('auth', createAuth({
    jwtSecret: c.env.JWT_SECRET,
    databaseUrl: c.env.DATABASE_URL,
  }))
  await next()
})

app.post('/auth/signin', async (c) => {
  return authHonoSignIn(c.get('auth'))(c)
})

The auth instance is built per-request from the bindings; create it once per worker if you can amortise the cost.

Error handling

import { HTTPException } from 'hono/http-exception'
import { AuthError } from '@gentleduck/auth/core'

app.onError((err, c) => {
  if (err instanceof AuthError) {
    return c.json(err.toJSON(), err.status)
  }
  if (err instanceof HTTPException) {
    return err.getResponse()
  }
  console.error(err)
  return c.json({ error: 'internal' }, 500)
})