{
  "slug": "bw-dev",
  "name": "BW Dev",
  "version": "1.16.0",
  "download_url": "https://plugins.bowden.works/wp-content/uploads/plugin-updates/bw-dev-1.16.0.zip",
  "download_hash": "sha256:9478b44941b892cc9aa87e81eb2c7b289bdb3fbb704e4676c2a37af38db30d54",
  "download_size": 501406,
  "requires": "6.0",
  "tested": "",
  "requires_php": "7.4",
  "last_updated": "2026-06-09",
  "homepage": "https://plugins.bowden.works/bw-dev/",
  "author": "Bowden Works",
  "description": "Bowden Works dev toolkit — admin columns, favicon, sticky elements, post-link blocks, YouTube embed, menu visibility, SVG uploads, admin notes, and Flywheel auto-update support in one actively-maintained plugin. Required for several site features. See Settings → BW Dev → About.",
  "changelog": "## [1.16.0] - 2026-06-09\n\n### Added\n- **Request Firewall module** (`request_firewall`, group `core`) — returns an instant `410 Gone` for unambiguous junk / exploit-probe URLs BEFORE WordPress loads plugins, the theme, or runs the main query, so a flood of junk requests can't exhaust Flywheel's small PHP worker pool and 504 the site (the June 2026 failure mode on theacademy.ca.gov + communityfinancials.com: a ~57k-URL backup/secret-file scan forced ~57k full ~2.3s bootstraps and starved real visitors). **Architecture:** bw-dev stays a normal plugin but owns and manages a tiny, self-contained mu-plugin at `wp-content/mu-plugins/bw-request-firewall.php` (the Wordfence pattern) — the stub is dependency-free (reads only `$_SERVER`/`$_COOKIE` + a baked-in pattern list) so it keeps working even if bw-dev is deactivated. **Why matching is safe:** on Flywheel's nginx, if a real backup/vendor/secret file existed it would be served directly and never reach PHP — so any such URL reaching the firewall is, by construction, a non-existent file = a probe. The stub matches the request PATH only (query string stripped, one `rawurldecode` for encoding-evasion resistance), skips any request carrying a `wordpress_logged_in_` cookie (logged-in users are never scanners — bounds false-block blast radius), and uses `@preg_match` so even a malformed pattern fails OPEN. **Management layer:** canonical pattern list with a `bw_firewall_patterns` filter for per-site additions (validated + baked into the stub at generation time); fingerprint-gated self-heal on `admin_init` (rate-limited to once/hour, forced after updates via `upgrader_process_complete`) that rewrites the stub when the deployed file's embedded fingerprint differs from `hash(FIREWALL_VERSION + final patterns + diagnostic flag)` or the file is missing — so deletion self-heals and filter additions/diagnostic toggles trigger a rebuild without a version bump; `WP_Filesystem`-based deploy that never fatals and shows an admin notice if `mu-plugins/` is unwritable. **Settings tab:** install status / version / writability, a \"test a URL\" box (paste a real URL to confirm it is NOT blocked, or a probe to confirm it is — reports the matched rule), the active pattern list, Regenerate / Remove buttons, an \"on deactivate\" toggle (default: leave the stub so protection persists), and a diagnostic-logging toggle (off by default; when on, the stub is regenerated WITH sampled 1-in-20 logging baked in — never touches the DB on the hot path). **Emergency kill switch:** `define( 'BW_FIREWALL_OFF', true )` in wp-config.php disables the stub instantly. Conservative default list (backup/archive/dump extensions, `.env`, VCS metadata dirs, wp-config variants, PHP files under `/vendor/` and `/wp-content/uploads/`, the PHPUnit eval-stdin RCE probe, install.php, classic secret files) — deliberately EXCLUDES `.zip` (download plugins stream it) and bare `/vendor/` (a site could have a real \"Our Vendors\" page). Blocked junk returns `X-BW-Firewall: 1` + `X-BW-Firewall-Rule: <label>` headers for access-log observability. Uninstall removes the stub. **Front line stays Cloudflare** (WAF + cache + origin lock); this is the in-WordPress backstop that also catches direct-to-origin hits which bypassed Cloudflare on June 8."
}
