import * as hono from 'hono'; import { Context } from 'hono'; /** * Pluggable storage adapter for IP strike tracking and banning. * Implement this interface for your storage backend (Redis, KV, SQLite, etc.) * * All methods may return a value or a Promise — the middleware awaits uniformly. * * @example Redis adapter * ```ts * const store: HoneypotStore = { * async isBanned(ip) { * return await redis.exists(`honeypot:ban:${ip}`) === 1; * }, * async addStrike(ip) { * const key = `honeypot:strikes:${ip}`; * const count = await redis.incr(key); * if (count === 1) await redis.expire(key, 3600); * return count; * }, * async ban(ip) { * await redis.setex(`honeypot:ban:${ip}`, 86400, '1'); * }, * async resetStrikes(ip) { * await redis.del(`honeypot:strikes:${ip}`); * } * }; * ``` */ interface HoneypotStore { /** Check if an IP is currently banned. Called BEFORE pattern matching (fast path). */ isBanned(ip: string): Promise | boolean; /** Record a strike against an IP. Return the new total count. Called on pattern match. */ addStrike(ip: string): Promise | number; /** Ban an IP. Called when strikes reach the threshold. */ ban(ip: string): Promise | void; /** Clear strikes for an IP. Called after a ban is set. */ resetStrikes(ip: string): Promise | void; } /** * Information about a blocked request, passed to the onBlocked callback */ interface BlockInfo { /** Client IP address */ ip: string; /** Normalized request path */ path: string; /** HTTP method (GET, POST, etc.) */ method: string; /** Why the request was blocked: pattern match or IP ban */ reason: 'pattern' | 'banned'; /** Current strike count (only present for pattern matches when store is active) */ strikes?: number; /** Whether this strike triggered a new ban */ banned?: boolean; } /** * Configuration options for honeypot middleware */ interface HoneypotOptions { /** * Add custom attack patterns to block (merged with built-in patterns) * * @example * ```ts * patterns: [ * /^\/custom-admin/i, // Block /custom-admin * /^\/internal/i, // Block /internal * ] * ``` */ patterns?: RegExp[]; /** * Exclude specific built-in patterns (useful for allowing legitimate routes) * * @example * ```ts * // Allow your own /admin dashboard but keep other admin patterns blocked * exclude: [/^\/admin$/i] * ``` */ exclude?: RegExp[]; /** * Log blocked requests to console with emoji and client IP * * @default true * * When onBlocked is provided, built-in logging is suppressed regardless of this setting. * * @example * Output: `Blocked [192.168.1.1] GET /wp-admin` */ log?: boolean; /** * HTTP status code to return for blocked requests * * @default 410 Gone (fastest bot deterrence + search engine deindexing) * * - **410 Gone**: Signals permanent removal, bots stop retrying faster, Google/Bing prioritize for index removal * - **404 Not Found**: Standard response but encourages bot retry logic * - **403 Forbidden**: May trigger escalation attempts by sophisticated scanners * * Whichever you pick, the response ships `Cache-Control: no-store` and `CDN-Cache-Control: * no-store` and that is not configurable. See {@link BLOCK_HEADERS}: a blocked response is a * statement about the caller, so letting a CDN store it under a URL key would serve one * visitor's block to everyone else. That matters most with the default 410, precisely because * search engines act on it fastest. */ status?: 410 | 404 | 403; /** * Pluggable store for IP strike tracking and banning. * When provided, enables the strike/ban system. * Without a store, the middleware is stateless (pattern match only). * * @example * ```ts * import { honeypot, MemoryStore } from 'hono-honeypot' * app.use('*', honeypot({ store: new MemoryStore() })) * ``` */ store?: HoneypotStore; /** * Number of pattern-match strikes before an IP is banned. * Banned IPs get blocked on ALL paths (fast path, no pattern matching needed). * @default 3 */ strikeThreshold?: number; /** * Extract client IP from the request context. * Default checks: cf-connecting-ip > x-forwarded-for > x-real-ip > 'unknown' * IPs resolving to 'unknown' are not tracked (prevents false bans). * * @example * ```ts * // Use Hono's built-in IP resolution * getIP: (c) => c.req.header('x-real-ip') || 'unknown' * ``` */ getIP?: (c: Context) => string; /** * Called when a request is blocked (pattern match or ban). * Use for custom logging, webhooks, metrics, etc. * When provided, built-in console.log is suppressed. * * The Hono `Context` is passed as a second argument so handlers can read * request data or environment bindings (e.g. `c.env.ABUSEIPDB_API_KEY` on * Cloudflare Workers, where `process.env` is empty). * * @example * ```ts * onBlocked: (info, c) => { * console.log(`[honeypot] ${info.reason}: ${info.ip} ${info.method} ${info.path}`); * if (info.banned) analytics.track('ip_banned', { ip: info.ip }); * } * ``` */ onBlocked?: (info: BlockInfo, c: Context) => void | Promise; } /** * In-memory store for development and single-process deployments. * Uses lazy expiry (checks on read, no timers). * * NOT suitable for multi-process, clustered, or serverless environments * where each isolate has its own memory. Use a Redis or KV-backed store * for production distributed deployments. * * @example * ```ts * import { honeypot, MemoryStore } from 'hono-honeypot' * * app.use('*', honeypot({ * store: new MemoryStore({ strikeTTL: 3600, banTTL: 86400 }) * })) * ``` */ declare class MemoryStore implements HoneypotStore { private strikes; private bans; private strikeTTL; private banTTL; constructor(options?: { /** Strike window in seconds. Strikes reset if no new attacks within this period. @default 3600 (1 hour) */ strikeTTL?: number; /** Ban duration in seconds. @default 86400 (24 hours) */ banTTL?: number; }); isBanned(ip: string): boolean; addStrike(ip: string): number; ban(ip: string): void; resetStrikes(ip: string): void; } /** * Create honeypot middleware to block bot attacks and vulnerability scanners * * Intercepts 200+ common attack patterns (WordPress, PHP, admin panels, framework probes, etc.) * before they reach your route handlers. Returns 410 Gone by default for faster search engine * deindexing and bot deterrence. * * When a store is provided, enables IP strike tracking: after N pattern matches (default 3), * the IP is banned and ALL subsequent requests return 410 instantly without pattern matching. * * Every blocked response is sent uncacheable ({@link BLOCK_HEADERS}). A ban blocks a banned caller * on paths that DO exist, so a cached block would leak one visitor's verdict to everyone behind the * same CDN edge. * * @param options - Configuration for patterns, store, logging, and status code * @returns Hono middleware handler * * @example * Basic usage (stateless, blocks all built-in patterns) * ```ts * import { Hono } from 'hono' * import { honeypot } from 'hono-honeypot' * * const app = new Hono() * app.use('*', honeypot()) * ``` * * @example * With IP banning via MemoryStore * ```ts * import { honeypot, MemoryStore } from 'hono-honeypot' * * app.use('*', honeypot({ * store: new MemoryStore(), * strikeThreshold: 3, * })) * ``` */ declare const honeypot: (options?: HoneypotOptions) => hono.MiddlewareHandler>; export { type BlockInfo, type HoneypotOptions, type HoneypotStore, MemoryStore, honeypot };