⚠️ Warning: Still under development.
Proof-of-work (PoW) gate with honeypot and content obfuscation for Astro sites. A minimal integration that deters automated scrapers through client-side challenges without requiring external services.
Astro Shield is a client-side obfuscation layer designed to deter automated scrapers and basic bots from static websites. It verifies visitors through computational challenges that run entirely in the browser, requiring no backend infrastructure, no APIs, or third-party services.
- ✅ Simple scrapers and automated tools with JavaScript execution
- ✅ Basic crawlers without JavaScript capability (cannot render shielded content)
- ✅ Automated form submissions and content harvesting
- ✅ Unsophisticated bot traffic that floods small independent sites
- ❌ Advanced bots with headless browser capabilities and PoW solving
- ❌ Determined attackers willing to solve computational challenges
- ❌ Sophisticated AI scraper infrastructure with anti-detection measures
- You run a static site and need basic bot protection without server-side code
- You want to avoid external dependencies like Cloudflare or commercial anti-bot services
- You need a lightweight, self-contained solution for small-to-medium traffic sites
- You're willing to trade some legitimate bot access (search engines, archivers) for reduced scraper load
- You need enterprise-grade bot protection for high-value targets
- You require allowlisting for "good bots" like search engines or the Internet Archive
- You're under active attack from sophisticated scraping operations
- You need backend validation and server-side enforcement
- Anubis: Open-source backend firewall utility that validates connections before they reach your server, with configurable bot policies and allowlisting
- Cloudflare: Enterprise-grade CDN with advanced bot management, DDoS protection, and WAF capabilities
This is a client-side obfuscation and deterrent approach that raises the cost of automated access. It won't stop determined adversaries, but it will significantly reduce noise from the vast majority of unsophisticated scrapers that target the independent web.
- Proof-of-Work Gate: Challenge-based protection that requires computational work to access your site
- Honeypot Defenses: Invisible form fields that detect automated form filling
- Decoy Links: Trap links (e.g.,
/admin,/wp-admin) that catch scrapers and crawlers - Time-Based Validation: Detect suspiciously fast form submissions
- Zero Dependencies: No external services or API keys required
- Fully Type-Safe: Complete TypeScript support
- Configurable: Flexible options for difficulty, timeouts, and behavior
- Auto-Hide Root: Optionally redirect the root page to the gate
- Shielded Content Blocks: Wrap critical markup so it only renders after the gate validates
- Native Astro Integration: Seamless setup with Astro's integration system
- Gate: Visitors are redirected to a proof-of-work challenge page
- PoW: Client-side JavaScript performs computational work to find a valid solution
- Token: A valid solution generates a time-limited token stored in localStorage
- Page Protection: Once the token is validated, users can access all protected pages
- Image Protection: Images using the
<ShieldedImage>component are only loaded after gate validation - they initially show a placeholder and only load the real image once the token is verified - Honeypots: Hidden form fields detect automated form filling
- Decoys: Invisible links to common bot targets (e.g.,
/admin,/wp-admin,/phpmyadmin) catch scrapers - any interaction with these links triggers protection - Time Validation: Tracks form submission speed to detect bots that fill forms too quickly
Important Limitation: This protection works by controlling when content is rendered in the browser. If a bot already knows the direct URL to an image or asset (e.g., from a sitemap, previous crawl, or source code inspection), it can bypass the gate by requesting that URL directly. Image protection is most effective for content where URLs are not predictable or publicly listed.
When a bot trips a honeypot or decoy link:
- The violation is logged with a unique reason code
- The bot is immediately redirected back to the gate
- The PoW difficulty is increased by the
honeypotPenaltyvalue (default: +1 difficulty level) - Multiple violations stack, making it exponentially harder for bots to bypass protection
- This creates an adaptive defense that becomes harder the more a bot misbehaves
# Using bun
bun add @meeghele/astro-shield
# Using npm
npm install @meeghele/astro-shield
# Using pnpm
pnpm add @meeghele/astro-shield
# Using yarn
yarn add @meeghele/astro-shieldAdd the integration to your astro.config.mjs:
import { defineConfig } from 'astro/config';
import { astroShield } from '@meeghele/astro-shield';
export default defineConfig({
integrations: [
astroShield({
gatePath: '/gate',
autoHideRoot: true,
shield: {
difficulty: 4,
enableHoneypots: true,
}
})
]
});That's it! Your site now requires visitors to pass the proof-of-work gate at /gate before accessing protected content.
Take a look at the default configuration in Gate.astro.
interface AstroShieldOptions {
gatePath?: string; // Path to the gate page (default: "/gate")
autoHideRoot?: boolean; // Redirect root to gate (default: true)
shield?: ShieldConfig; // Advanced shield configuration
}All settings are optional. Here are the defaults with descriptions:
{
// Integration settings
gatePath: '/gate', // Path to the gate page
autoHideRoot: true, // Redirect root to gate
// Shield settings
shield: {
// Proof-of-work settings
difficulty: 8, // PoW difficulty (8=very easy, 12=easy, 16=medium, 20=hard)
timeoutMs: 8000, // Max time to solve PoW (8 seconds)
minSolveDurationMs: 1000, // Minimum solve time to prevent precomputed solutions (1 second)
// Token management
tokenTtlMinutes: 30, // Token validity duration (30 minutes)
nearMissThreshold: 4, // Near-miss detection threshold for slow devices
minAcceptable: 4, // Minimum acceptable hash value
enableNearMisses: true, // Allow near-miss solutions for slow devices
// Honeypot protection
enableHoneypots: true, // Enable honeypot traps
enableInputHoneypots: true, // Enable input field honeypots (hp1-hp5)
enableLinkDecoys: true, // Enable decoy link honeypots (/admin, /login, etc.)
honeypotPenalty: 1, // Penalty difficulty added for triggering honeypots
maxPenaltyDiff: 16, // Maximum penalty difficulty
honeypotPrefix: 'hp', // Honeypot reason code prefix (obfuscatable)
decoyPrefix: 'dc', // Decoy reason code prefix (obfuscatable)
shieldNamespace: 'as', // localStorage namespace (obfuscatable)
// Honeypot/decoy element customization (for obfuscation)
honeypotIds: ['hp1', 'hp2', 'hp3', 'hp4', 'hp5'], // Element IDs for honeypot inputs (obfuscatable)
decoyIds: ['decoy1', 'decoy2', 'decoy3'], // Element IDs for decoy links (obfuscatable)
honeypotClass: 'honeypot', // CSS class for runtime querying (obfuscatable)
honeypotStyleClasses: ['hp', 'hp-visible', 'hp-css'], // CSS style classes (obfuscatable)
// Validation
enableFinalCheck: true, // Enable final validation check before token creation
enableTimeValidation: true, // Validate solve duration and timing
// UX settings
redirectTo: '/', // Redirect destination after solving
redirectDelayMs: 5000, // Delay before redirect (5 seconds)
showProgress: true, // Show solving progress bar
showDebugInfo: false, // Display debug information in console
// Theme colors (Gate component only)
darkBgColor: '#0e141b', // Dark mode background color
darkTextColor: '#d6d3d1', // Dark mode text color
darkBarColor: '#facc15', // Dark mode progress bar color
lightBgColor: '#ffffff', // Light mode background color
lightTextColor: '#374151', // Light mode text color
lightBarColor: '#f97316', // Light mode progress bar color
themeClassName: 'theme-sleek', // Theme class name for dark/light mode compatibility
}
}- Development/Testing (no honeypots for clean testing):
{ difficulty: 4, timeoutMs: 20000, showDebugInfo: true, enableHoneypots: false } - Light Protection (fast, user-friendly with basic honeypots):
{ difficulty: 8, timeoutMs: 10000, tokenTtlMinutes: 60, enableFinalCheck: false } - Medium Protection (balanced with all honeypot types):
{ difficulty: 12, timeoutMs: 8000, tokenTtlMinutes: 30 } - Strong Protection (slower, more secure with aggressive honeypots):
{ difficulty: 16, timeoutMs: 5000, honeypotPenalty: 3, maxPenaltyDiff: 20 } - Maximum Protection (may frustrate some users):
{ difficulty: 20, timeoutMs: 3000, honeypotPenalty: 4, maxPenaltyDiff: 24 } - Honeypot-Only Mode (no PoW, just honeypot detection):
{ difficulty: 0, timeoutMs: 1000, enableFinalCheck: true, enableInputHoneypots: true } - No-Honeypot Mode (PoW only, for environments with form auto-fill issues):
{ difficulty: 12, enableHoneypots: false } - Obfuscated Honeypots (harder for bots to detect):
{ honeypotIds: ['backup_email', 'alt_contact', 'secondary_url', 'prefs_newsletter', 'extra_comments'], decoyIds: ['help_link', 'support_portal', 'resources'] }
<ShieldedContent> keeps critical markup inside a <template> until the gate script unlocks the page. Users with JavaScript disabled see a friendly fallback message, while bots that fetch raw HTML never get the rendered content.
---
import ShieldedContent from '@meeghele/astro-shield/components/ShieldedContent.astro';
---
<ShieldedContent fallback="Please enable JavaScript to continue.">
<main>
<h1>Protected area</h1>
<p>Only visible after the PoW gate passes.</p>
</main>
</ShieldedContent>- Content is delivered inside a
<template>so non-JS clients cannot render it. - The fallback message remains visible when scripts are blocked.
- Once the gate validates a token, the runtime dispatches an
astro-shield:unlockedevent and<ShieldedContent>swaps the real markup into the DOM.
Important: This is content obfuscation, not access control. The HTML ships in the response inside
<template>tags. Determined scrapers that parse templates or extract data attributes can bypass this entirely. Use this to deter unsophisticated bots, not to secure sensitive content.
The <ShieldedImage> component prevents images from loading in the browser until the user passes the gate:
---
import ShieldedImage from '@meeghele/astro-shield/components/ShieldedImage.astro';
import myImage from '../assets/photo.jpg';
---
<!-- Shielded image - requires valid token to load in browser -->
<ShieldedImage src={myImage} alt="Protected content" />
<!-- Standard image - loads immediately when page is accessed -->
<Image src={myImage} alt="Public content" />- The real image URL is stored in a
data-srcattribute instead ofsrc - A placeholder is shown initially
- Once the user passes the gate and has a valid token, JavaScript swaps
data-srctosrcand loads the image - Right-click, drag-and-drop, and other browser shortcuts are disabled
By default, honeypot elements use obvious IDs and prefixes like hp1, hp2, decoy1, as (namespace), hp (prefix), dc (decoy prefix) which sophisticated bots can recognize. You can customize these to make them less detectable:
{
// Use innocuous-looking IDs instead of obvious ones
honeypotIds: ['backup_email', 'alt_contact', 'secondary_url', 'prefs_newsletter', 'extra_comments'],
decoyIds: ['help_link', 'support_portal', 'resources'],
honeypotClass: 'field-optional',
honeypotStyleClasses: ['visually-hidden', 'sr-only', 'offscreen'],
// Obfuscate prefixes and namespace
shieldNamespace: 'myapp', // Default: 'as'
honeypotPrefix: 'trap', // Default: 'hp'
decoyPrefix: 'bait', // Default: 'dc'
}Important: All five honeypot IDs and all three decoy IDs must be provided if you customize them. The array length must match the defaults.
For maximum security, generate random values at build time so they change with every deployment:
import { randomBytes } from 'crypto';
const generateRandomString = (length = 8) =>
randomBytes(length).toString('hex').slice(0, length);
export default defineConfig({
integrations: [
astroShield({
shield: {
// Generate random values on each build
shieldNamespace: generateRandomString(),
honeypotPrefix: generateRandomString(),
decoyPrefix: generateRandomString(),
honeypotIds: Array.from({ length: 5 }, () => generateRandomString(10)),
decoyIds: Array.from({ length: 3 }, () => generateRandomString(10)),
honeypotClass: generateRandomString(12),
honeypotStyleClasses: Array.from({ length: 3 }, () => generateRandomString(12)),
}
})
]
});This approach makes it nearly impossible for bots to fingerprint your honeypot elements since they change with every build.
Both standard and shielded images have their URLs visible in the HTML source code. This means:
- ✅ ShieldedImage prevents: Images from loading in the browser before gate validation
- ✅ ShieldedImage prevents: Simple bots that only process
srcattributes - ❌ ShieldedImage does NOT prevent: Bots that parse HTML and extract URLs from
data-srcattributes - ❌ ShieldedImage does NOT prevent: Direct requests to image URLs if the bot already knows them
<ShieldedImage> improves user experience by hiding content until validation, and adds a minor hurdle for unsophisticated scrapers. It is not a security feature against determined bots that can parse HTML or make direct HTTP requests to image URLs.
You can test Astro Shield locally using the included demo app:
# Navigate to the app directory
cd app
# Install dependencies
bun install
# Start the development server
bun run devThe app demonstrates the gate flow with honeypot protection and shielded images. Visit http://localhost:4321 to see the PoW gate in action.
# Install dependencies
bun install
# Build
bun run build
# Lint
bun run lintThis project is licensed under the MIT License - see the LICENSE file for details.
Michele Tavella - meeghele@proton.me