Access control middleware for Hono leveraging Cloudflare Workers' request.cf properties.
Add country blocking, ASN blocking, and maintenance mode to any route with a single line.
request.cfnative — Uses geo data Cloudflare Workers provides for free- Declarative API — Declare deny/allow lists, no hand-written conditionals
- Three middlewares —
countryBlock(),asnBlock(),maintenance() - RFC 9457 compliant —
application/problem+jsonerror responses - Customizable —
onDenied/onMaintenanceescape hatches for custom responses fallbackoption — Controls behavior whenrequest.cfis undefined (local dev)cfInfocontext variable — Normalized geo data accessible from handlers- Zero external dependencies — Only requires Hono as a peer dependency
npm install hono-cf-accessimport { Hono } from 'hono'
import { countryBlock } from 'hono-cf-access'
const app = new Hono()
// Deny access from specific countries
app.use('/api/*', countryBlock({
deny: ['CN', 'RU'],
}))
// Or allow only specific countries
app.use('/api/*', countryBlock({
allow: ['JP', 'US', 'GB'],
}))import { asnBlock } from 'hono-cf-access'
// Deny access from specific ASNs
app.use('/api/*', asnBlock({
deny: [4134, 4837],
}))
// Or allow only specific ASNs
app.use('/api/*', asnBlock({
allow: [13335, 209242],
}))import { maintenance } from 'hono-cf-access'
// Static toggle
app.use('/api/*', maintenance({
enabled: true,
}))
// Dynamic toggle via KV
app.use('/api/*', maintenance({
enabled: async (c) => {
const kv = c.env.MAINTENANCE_KV as KVNamespace
return (await kv.get('maintenance_mode')) === 'true'
},
}))
// With IP allowlist for admin access
app.use('/api/*', maintenance({
enabled: true,
allowedIps: [
'203.0.113.50',
'192.168.1.0/24',
],
retryAfter: 3600,
}))app.use('/api/*',
countryBlock({ deny: ['CN', 'RU'] }),
asnBlock({ deny: [4134] }),
maintenance({ enabled: async (c) => { /* ... */ } }),
)Each middleware operates independently. If one denies the request, subsequent ones are not executed.
All middlewares set a cfInfo context variable with normalized geo data:
app.get('/api/info', (c) => {
const info = c.get('cfInfo')
// info.country → 'JP'
// info.asn → 13335
// info.city → 'Tokyo'
// info.timezone → 'Asia/Tokyo'
return c.json(info)
})countryBlock({
deny: ['CN'],
onDenied: (c) => c.html('<h1>Access Denied</h1>', 403),
})
maintenance({
enabled: true,
onMaintenance: (c) => c.html('<h1>Under Maintenance</h1>', 503),
})| Option | Type | Default | Description |
|---|---|---|---|
deny |
string[] |
— | Country codes to deny (ISO 3166-1 alpha-2) |
allow |
string[] |
— | Country codes to allow. All others denied |
fallback |
'allow' | 'deny' |
'allow' |
Behavior when request.cf is undefined |
onDenied |
(c: Context) => Response |
— | Custom response for denied requests |
denyandalloware mutually exclusive. Specifying both throws at initialization.
| Option | Type | Default | Description |
|---|---|---|---|
deny |
number[] |
— | ASN numbers to deny |
allow |
number[] |
— | ASN numbers to allow. All others denied |
fallback |
'allow' | 'deny' |
'allow' |
Behavior when request.cf is undefined |
onDenied |
(c: Context) => Response |
— | Custom response for denied requests |
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | (c: Context) => boolean | Promise<boolean> |
— | Whether maintenance mode is active |
allowedIps |
string[] |
— | IPs/CIDRs that bypass maintenance (IPv4 and IPv6) |
retryAfter |
number | string |
— | Retry-After header value |
fallback |
'allow' | 'deny' |
'allow' |
Behavior when IP cannot be resolved |
onMaintenance |
(c: Context) => Response |
— | Custom maintenance response |
interface CfInfo {
country?: string
asn?: number
city?: string
region?: string
regionCode?: string
continent?: string
latitude?: string
longitude?: string
timezone?: string
postalCode?: string
}Default responses follow RFC 9457 Problem Details (Content-Type: application/problem+json):
| Scenario | Status | Type |
|---|---|---|
| Country denied | 403 | /errors/country-denied |
| ASN denied | 403 | /errors/asn-denied |
| Maintenance | 503 | /errors/maintenance |
| CF data unavailable (strict) | 403 | /errors/cf-unavailable |
Example response:
{
"type": "https://hono-cf-access.dev/errors/country-denied",
"title": "Forbidden",
"status": 403,
"detail": "Access from country 'CN' is not allowed",
"instance": "/api/data"
}@hono/cloudflare-access: Validates Cloudflare Access JWT tokens (authentication)hono-cf-access: Access control usingrequest.cfgeo data (authorization/filtering)
MIT