- Security (gate completeness): Two coordinated-disclosure findings in the
action-gating model are closed. Affected versions: β€ 4.0.0 (both predate 4.0.0
β F2 since the plugin's inception, F1 since the multi-surface gate).
- Interactive effect-level backstop. The admin surface previously gated
only by request-pattern matching against enumerated core pages, so a
gated-equivalent destructive action invoked through a non-enumerated handler
(e.g. a third-party
admin-post.phproute or custom dispatcher) could run without a sudo challenge β even though the identical action was blocked on WP-CLI and on core's enumerated REST routes. A session-aware backstop now arms onadmin_initand hard-blocks the unambiguous destructive effect actions (delete_user,delete_plugin,delete_theme,activate_plugin,upgrader_pre_install,export_wp) when no sudo window is active. It is deliberately scoped to those effect hooks; thepre_update_option_*filters are excluded because WordPress core rewrites those options incidentally during ordinary admin loads. When blocked it fireswp_sudo_action_blockedon theadminsurface with the real user ID. (Custom REST routes and theuser.create/user.promotepaths are tracked as follow-up increments.) - Login-session binding. The sudo proof is now bound to the WordPress
login session that created it via the new
_wp_sudo_session_binduser-meta key (SHA-256 of the login-session token). A capturedwp_sudo_tokencookie can no longer be replayed from another session; the session is ended onwp_logout, and a bound proof stops verifying once its login session is no longer valid (e.g. afterWP_Session_Tokens::destroy_all()the user is no longer authenticated, so the window is unreachable). Binding is enforced only when a bind value is present, so existing sessions need no migration; cookie-less surfaces (CLI/cron/Application Passwords/WPGraphQL) carry no bind and remain governed by policy. The session is now also deactivated onwp_logout, and the login-session token is captured fromset_logged_in_cookieso sessions granted during the login request bind correctly.
- Interactive effect-level backstop. The admin surface previously gated
only by request-pattern matching against enumerated core pages, so a
gated-equivalent destructive action invoked through a non-enumerated handler
(e.g. a third-party
- Breaking changes (4.0.0):
sudo_can()removed. The deprecated unprefixed alias (deprecated in 3.3.0) no longer exists; calling it is a fatal undefined-function error. Usewp_sudo_can( string $cap, ?int $user_id = null ): boolβ identical signature. Search-replace any remainingsudo_can(calls withwp_sudo_can(.compatibilitygovernance mode removed. Governance is now always strict β capability checks delegate touser_can( $user_id, $cap )against the dedicatedmanage_wp_sudofamily. Compatibility mode was added in 3.2.0 as a transitional bridge when the dedicated-capability model replaced baremanage_optionschecks β it let sites keep the oldmanage_optionsauthority while administrators were migrated onto the new caps, avoiding lockout before the backfill ran. It is removed now that the model is the established default, the 3.3.0 backfill grants the caps automatically, and the 3.4.0-hardenedWP_SUDO_RECOVERY_MODEcovers the lockout-recovery case β collapsing governance to one auditable path. A site that setwp_sudo_governance_modeis now treated as strict, and the inert option is removed automatically β no manual cleanup needed:upgrade_4_0_0()deletes it (both option stores on multisite) on the 3.x β 4.0.0 boundary, and anadmin_initself-heal (cleanup_inert_governance_mode_option()) clears it if it reappears. After cleanup, an admin withmanage_wp_sudosees a one-time dismissible success notice (no persistent warning, no_doing_it_wrong()); the developer/audit signal is thewp_sudo_inert_governance_mode_detectedaction.WP_SUDO_RECOVERY_MODEremains the only break-glass path.- Minimum WordPress raised to 6.4 (from 6.2).
- Minimum PHP raised to 8.2 (from 8.0).
composer.jsonnow requiresphp >=8.2, and the CI matrix drops the 8.0/8.1 lanes.
- Connector credential writes gated on WordPress 7.0 (registry-aware
matcher): the
connectors.update_credentialsrule now matches connector API-key writes toPOST /wp/v2/settingswith a two-tier matcher. Tier 1 reads the WordPress 7.0 Connectors registry (wp_get_connectors(), guarded byfunction_exists()) and gates every setting name belonging to anapi_key-method connector; Tier 2 retains the existing^connectors_[a-z0-9_]+_api_key$regex as a union fallback. This closes a false-negative where connectors whose setting name does not match the regex β notably Akismet'swordpress_api_keyβ were ungated on every WP 7.0 install. The registry read is cached per request and re-read afterAction_Registry::reset_cache(). Verified against WordPress 7.0 GA. (CONN-01 through CONN-06.) - WordPress 7.0 upgrade-path fatal fixed:
Upgrader::maybe_upgrade()now primeswp_roles()before running migration routines. On WordPress 7.0,WP_User_Querydereferences the global$wp_rolesfor capability queries, which is not yet initialized atplugins_loadedunder WP-CLI/cron β so theupgrade_3_3_0()governance backfill (aget_users( array( 'capability' => 'manage_wp_sudo' ) )call) fataled withCall to a member function for_site() on nullwhen a site upgraded across the 3.3.0 boundary in a non-interactive context. Priming the global once makes the upgrade path safe regardless of how early it fires. - E2E CI balancing: the default Chromium Playwright workflow now uses four
explicit test groups instead of Playwright's opaque
--shardassignment. The heavy challenge-flow tests are split across basic/admin, 2FA UI, lockout/surface, and replay/multisite groups while preserving the aggregateE2E Testsrequired status check. - WordPress.org readiness: the listing name is set to "Sudo β Admin Action
Gating" (plugin header + readme title; the in-product UI brand stays "Sudo"
and the slug/text-domain stay
wp-sudo). AddsSECURITY.mdand a WordPress.org submission checklist, trims the readme short description to the 150-character limit, and reconciles the request-stash redaction status (the shipped matcher is exact-match plus suffix-based viaSENSITIVE_KEY_SUFFIXES, not exact-key only). Plugin Check (PCP) passes the plugin-name, trademark, and stable-tag rules. (ORG-01 through ORG-07.) - Screenshots (9): a deterministic, env-gated Playwright capture spec
(
npm run screenshots) regenerates the nine.wordpress-orglisting screenshots β challenge page, gated plugin activation, the four settings tabs (Settings, Gated Actions, Rule Tester, Access), the Session Activity dashboard widget, the admin-bar session timer, and the break-glass recovery notice β matching the readme== Screenshots ==captions one-to-one. - Manual release environment matrix:
tests/MANUAL-TESTING.mdgains a release environment matrix (an Apache stack, a managed WordPress host, and the minimum supported WordPress version) plus a Connectors-credential manual verification β cookie-auth and Application Password writes toPOST /wp/v2/settingswith connector credential fields, includingwordpress_api_key. (ENV-01 through ENV-03.)
- Break-glass recovery mode hardened (role-gated + visible):
WP_SUDO_RECOVERY_MODEpreviously granted the mastermanage_wp_sudocapability to any logged-in user. The grant is now role-gated β it applies only to users who also holdmanage_options(single-site) ormanage_network_options(multisite), so a locked-out administrator recovers while subscribers, editors, and other non-admins gain nothing. A permanent, non-dismissible notice now renders on the Sudo settings screen while recovery mode is active, and a newwp_sudo_recovery_mode_activeaudit hook fires (stored as a sampledrecovery_modeevent, at most one per user per hour), so break-glass usage is explicit, bounded, and auditable. - New audit hook
wp_sudo_recovery_mode_active: fires on each Sudo admin-page load while recovery mode is active. See the developer reference for the signature. - Psalm gate repaired: the type-coverage gate had been silently passing without analyzing anything β a top-level
exitinuninstall.phpaborted the Psalm run (exit 0, zero output).uninstall.phpis now excluded from analysis, a guard fails the gate loudly if no type-coverage figure is emitted, and the shepherd.dev type-coverage badge reports again (~96%). - CI hardening: least-privilege
permissionsblocks added to all workflows; documentation-only pull requests now skip the heavy unit/integration/Psalm/CodeQL/E2E jobs without deadlocking the required status checks. - Documentation audit: a feature-implementation audit reconciled the docs with the code β corrected confabulated AJAX handler names and the OTP-resend rate-limit description in the changelog, and replaced drift-prone hardcoded counts (audit hooks, help tabs) with links to
docs/current-metrics.md. - Playground demos: added recovery-mode and user-switching scenario blueprints for manual review.
- Fix: removed an obsolete Editor role-error notice workaround in the admin UI; fixed a random-order unit-test flake in the SSL-detection path.
- Governance backfill re-keyed to 3.3.0 (fixes strict-mode lockout): the migration that grants
manage_wp_sudoand the other governance capabilities to existing single-site administrators was keyed at3.1.0β a version that never had a public release (tags went v3.1.1 β v3.1.3 β v3.2.0). Sites upgrading from any public 3.1.x release skipped the backfill, leaving nomanage_wp_sudoholders and locking administrators out of Settings β Sudo in the default strict governance mode (recovery only viaWP_SUDO_RECOVERY_MODE). The routine is now keyed at3.3.0so it also runs once for sites already stamped3.2.0, and it skips when any user already holdsmanage_wp_sudo, preserving deliberate Access-tab grant/revoke configurations. - Audit column clamping:
Event_Storenow clampsevent,rule_id,surface, andipvalues to their schema column widths before insert, so over-length values from third-party rules truncate predictably in PHP instead of erroring (strict MySQL, dropping the audit row) or truncating silently in the database. wp_sudo_grant_session_on_loginfilter: the automatic sudo session granted on browser login can now be suppressed (returnfalse) for shared-terminal/kiosk hardening or SSO integrations. Default behavior is unchanged. Note for SSO integrators: suppressing the grant for users without a usable WordPress password makes gated actions unreachable for them β see the developer reference. Closes audit register item F17.
sudo_can()helper: Replaces directmanage_optionschecks across the plugin with a fine-grained capability helper that maps to the WordPress capabilities system.wp_sudo_is_recovery_mode()allows capability checks to short-circuit during emergency recovery flows.- Access tab: Settings β Sudo gains an Access tab for managing which roles and users can administer sudo settings.
wp_sudo_grant_capandwp_sudo_revoke_capAJAX handlers are registered and gated by the newoptions.wp_sudo_accessrule; audit hooks fire on both grant and revoke. - WordPress capability integration: Sudo capability assertions are now mapped to standard WordPress capability checks so external tools (WP-CLI, audit plugins) can evaluate them through
current_user_can(). - Capability-gated grant/revoke actions:
grant_capandrevoke_capAJAX actions are now registered in the action registry and gated by the challenge flow.
- 2FA lockout integrity: A correct password no longer clears failed-attempt counters while the 2FA challenge is still pending, so repeated password + bad-2FA cycles accumulate toward the intended lockout. Pending 2FA transients are cleared at the start of each new password submission to prevent orphaned transient accumulation. OTP resend requests are now rate-limited by a per-user resend count-cap over a 5-minute window to prevent email flooding via the Two Factor Email provider.
- WPGraphQL mutation detection: Limited-mode fallback classification now decodes JSON bodies, GET/form
queryparams, and multipartoperationspayloads; catches JSON-escaped and batched mutations; fails safe on unknown persisted operations; and avoids blocking queries that only mentionmutationin string arguments. CR and CRLF line terminators in GraphQL comments are now handled per spec, closing a tokenizer bypass. Escaped triple-quotes inside block strings are now parsed correctly. UTF-8 BOM prefixes are stripped before JSON decoding. Persisted-query / APQ requests with no inline document body are treated as mutations by default; a filter allows read-only persisted ops to opt out. - REST plugin gate covers folder-based plugins: The
plugin.activate,plugin.deactivate, andplugin.deleteREST matchers now use[^/]+(?:/[^/]+)?to match folder-style plugin slugs (e.g.akismet/akismet). Previously, the majority of real-world plugins using a folder layout were silently ungated on the REST surface. - PCRE fail-closed for built-in rules: A PCRE error on a built-in rule now gates the request (fail-closed) instead of silently passing it.
wp_sudo_rule_regex_errorfires for observability. - Non-interactive surface coverage for plugin settings: The
wp_sudo_settingsoption write is now blocked on CLI, Cron, and XML-RPC surfaces with an audit hook on interception, preventing unprompted policy downgrades from non-interactive contexts. - Admin email gating:
new_admin_emailandadmin_emailfield writes are now challenge-gated on interactive and REST surfaces. Admin email retargeting is a recognized account-takeover precursor. - Per-user IP lockout: Failed-attempt IP lockout is now keyed on
ip + user_idinstead of IP alone. A single account on a shared egress IP (NAT, VPN, CGNAT) can no longer sustain a DoS against all admins sharing that IP. - 2FA challenge path checks IP lockout:
handle_ajax_2fa()now checks the per-IP lockout at entry, matching the behavior of the password submission path. Previously, an active IP lockout was honored at the password step but ignored at 2FA entry. - Cookie Secure flag hardening: Session and 2FA cookies now respect
FORCE_SSL_ADMINas a Secure-flag fallback whenis_ssl()returns false (e.g. behind a TLS-terminating reverse proxy withoutX-Forwarded-Proto). Awp_sudo_cookie_securefilter allows operator override. Previously the Secure flag was absent in these configurations. - REST auth surface hardening: The REST interceptor now requires the request to be unauthenticated via App Password before trusting the cookie-auth branch, preventing a pivot where a request carries both cookie credentials and an App Password.
- Request stash minimization: Challenge replay now stores only rule-allowlisted POST fields, never stores
$_GETseparately, redacts compound secret names by suffix, and blocks automatic replay for unsafe or incomplete POST bodies. - App Password policy validation: Per-App-Password policy overrides now require UUID v4 format validation and confirmed existence in
WP_Application_Passwordsbefore persisting. Policy entries are automatically removed when the corresponding App Password is deleted via thewp_delete_application_passwordhook. - Dashboard widget capability check: The active-sessions user edit link is now only emitted when the current user holds
edit_userfor the target, preventing link exposure on multisite where site admins cannot edit network users. - Public API cross-user isolation:
Public_API::check()now explicitly returns false when the target user ID differs fromget_current_user_id(), making the cross-user isolation guarantee explicit at the API boundary rather than relying on internal session-token rejection alone. - Built-in rule filter visibility: The
wp_sudo_gated_actionsfilter now emits a diagnostic action and Site Health warning when built-in rule IDs are missing after filtering. Intentional removal remains supported, but accidental wholesale erasure is visible to operators. - Uninstall defense-in-depth:
uninstall.phpnow assertsdelete_pluginsfor browser/admin execution while preserving WP-CLI uninstall behavior alongside the WordPress-providedWP_UNINSTALL_PLUGINsentinel.
wp sudo statusoutput: The status command now clarifies that the "active" state reflects the stored expiry timestamp only β token binding cannot be verified from CLI without cookie access.
- Inline script encoding: Dashboard widget inline JS i18n now uses
JSON_HEX_TAG | JSON_HEX_AMPencoding flags so<script>injection safety is explicit rather than relying on PHP's incidental\/escaping of forward slashes. - WPGraphQL block-string escaping documented:
developer-reference.mdnow documents that the only GraphQL block-string escape sequence is\"""(escaped triple-quote);\\has no special meaning inside block strings. - Admin help copy clarifies auth boundaries: Contextual help and public docs now distinguish reauthentication from authorization: Sudo verifies the current user is still the account holder; WordPress and target handlers still decide whether that user is allowed to perform the action.
- E2E CI acceleration: The default Chromium Playwright suite now runs in four GitHub Actions shards with a preserved aggregate
E2E Testsstatus check, and the roadmap captures follow-up cache/image/smoke-split options.
- Release Playground link: the stable release Blueprint installs the tag ZIP through
pluginDatainstead of using Playground's currently brittlegit:directorytag fetch path. - Playground link posture: README Playground links now distinguish the immutable latest-release demo from the current
maindemo. - Blueprint password seeding: the demo Blueprint now uses WordPress core's
wp_set_password()API instead of writing the password hash directly through$wpdb.
- Playground authentication: the demo Blueprint now resets the
adminuser password before login soadmin/passwordworks for both WordPress login and WP Sudo reauthentication. - Front-end toolbar cancellation: clicking the Sudo toolbar item now cancels an active sudo session from front-end admin-bar contexts without navigating away unexpectedly.
- Dashboard widget freshness: active-session transients are invalidated when sessions are cancelled, so the dashboard widget updates without needing a manual refresh.
- Demo activity: Playground now seeds recent privilege-action samples and active demo sudo sessions with staggered 5-15 minute durations.
- PR preview links: the Playground Preview workflow now uses the checked-in Blueprint and pins the plugin install to the PR commit through Playground's CORS-safe
git:directoryresource.
- Role-change interception: role and capability metadata writes are now blocked before mutation when they require an active sudo session, closing the gap where non-interactive role changes could be detected only after the write path.
- Sensitive request replay safety: intercepted requests that include password/secret fields no longer replay partial POST data after those fields are omitted from the stash; users are returned with a warning instead.
- MU-plugin loader resilience: copied MU shims now preserve the actual plugin loader path, and the static shim can recover when the plugin directory is renamed.
- Audit bridge parity: Stream and WP Activity Log bridges now include
wp_sudo_action_passedevents, keeping active-session approvals visible in downstream audit logs.
- PHP 8.0 test compatibility: reflection-based unit tests now avoid PHP 8.1-only reflection behavior when running under PHP 8.0.
- NPM security audit cleanup: updated vulnerable transitive development dependencies, including
fast-xml-parservia the repo override, and cleared the npm audit report.
- Release posture: refreshed WordPress 7.0 schedule references and kept public metadata aligned with WordPress 6.9 as the latest stable line until 7.0 final ships.
- Major milestone: operator tooling and visibility β WP Sudo now includes a Request / Rule Tester for representative admin, AJAX, and REST request shapes plus a Session Activity Dashboard Widget for active sessions, recent events, and current policy posture.
- Major milestone: policy control β Settings β Sudo now includes one-click Normal, Incident Lockdown, and Headless Friendly presets for the non-interactive surfaces, with confirmation, audit logging, and summary notices.
- Major milestone: ecosystem hardening β Connectors API credential writes saved through
/wp/v2/settingsnow require sudo when they includeconnectors_*_api_keyfields, protecting database-backed connector credentials without over-gating unrelated settings writes.
- Event persistence layer β audit events are now recorded through
Event_StoreandEvent_Recorder, enabling the dashboard widget and future reporting. The sharedwpsudo_eventstable includes 14-day retention, daily cron pruning, graceful degradation when the table is unavailable, and SQLite compatibility for Playground-style environments.
- Challenge lockout expiry recovery β corrected an edge case where the visible countdown could reach zero while the server still treated the lockout as active for that exact second, blocking an immediate retry. Password and IP lockouts now expire in sync with the countdown.
- Stale challenge and 2FA recovery flows β hardened recovery when a sudo session is already active or a user is returning from 2FA throttle/lockout flows, with expanded browser coverage for replay, resend, cancel, and recovery behavior.
- Active sessions: identity context β sessions panel now shows gravatars, username, role badge, display name, and time remaining for each active session. Responsive layout hides gravatars and names on small screens.
- Recent events: client-side filtering β dropdown filters for Time (1h / 24h / 7d), Event type, and Surface, applied client-side against 50 stored events. Filters laid out horizontally in a single row.
- Passed-event audit visibility defaults β
wp_sudo_action_passedevents (admin, REST, WPGraphQL) are now recorded by default so active-session actions stay visible in the audit timeline. Disabling passed-event logging now requires an explicit code override (constant/filter), and WP Sudo shows a warning notice when that override is active. - Widget placement and layout β widget renders in the side column at high priority, active session cards use CSS Grid (
repeat(auto-fit, minmax(180px, 1fr))) with scrollable container, usernames link to user-edit.php, and the empty-state panel now uses a clearer Site Healthβstyle status layout. - Users list "Sudo Active" filter β the Users β All Users screen gains a "Sudo Active (N)" view link that filters the list to users with an active sudo session via
_wp_sudo_expiresmeta query.
- Dashboard widget table semantics β added
scope="col"to table headers and screen-reader-only<caption>elements for the Recent Events and Policy Summary tables.
- WordPress 7.0 readiness β forward test and preview lanes are now pinned to
7.0-RC1, with RC1 visual signoff recorded and the remaining RC/GA checklist documented for final release-day verification. - Testing and compatibility breadth β added scheduled WordPress
6.3β6.6compatibility coverage, explicit nginx + php-fpm + MariaDB and Playground SQLite browser smoke workflows, and a dedicated nginx + MariaDB multisite smoke lane. - Testing workflow: local integration fallback β
composer test:integrationnow falls back to the runningwp-envtests-clicontainer when a local rebuild leaves the generated host-side MySQL endpoint stale, while CI continues to use the normal direct PHPUnit path. - Testing posture: expanded CI and browser coverage shipped with this release; live suite counts are tracked in
docs/current-metrics.md.
- Feature: Playwright end-to-end coverage β added browser-verified challenge, cookie, gate UI, admin bar timer, keyboard shortcut, MU-plugin AJAX, multisite network-admin, and visual-regression coverage to exercise the real user flows around reauthentication.
- Fix: multisite symlink and network-admin flow hardening β preserved network-admin return URLs and supported symlinked local multisite installs used in Local and Studio-style development.
- Fix: bootstrap plugin URL handling β plugin asset URLs now preserve normal
plugins_urlfiltering and custom plugin roots instead of assuming a fixed/wp-content/plugins/path. - Testing workflow: Local socket support β
bin/install-wp-tests.shcan now auto-detect a single Local by Flywheel MySQL socket when TCP MySQL is unavailable, with updated contributor guidance for local integration setup. - Repo hygiene β added GPL license and repository health files, and centralized live test/size counts in
docs/current-metrics.md. - 504 unit tests, 1311 assertions. 140 integration tests in CI.
- Feature: IP + user multidimensional rate limiting β failed authentication attempts are now tracked per-IP alongside per-user. When the same IP address triggers failures across multiple user accounts, the IP itself is locked out, mitigating credential-stuffing attacks that rotate usernames. The
wp_sudo_lockouthook now includes the triggering IP address as a third positional argument ($user_id, $attempts, $ip) for audit visibility. - Docs alignment β updated
security-model.mdwith the new rate-limiting dimensions anddeveloper-reference.mdwith the enriched lockout hook payload schema. Manual testing guide expanded with IP-based lockout verification steps. - 496 unit tests, 1293 assertions. 132 integration tests in CI.
- Feature: WP-CLI operator commands β added
wp sudo status,wp sudo revoke --user=<id>, andwp sudo revoke --allfor session inspection and revocation workflows. - Feature: Stream audit bridge β added optional
bridges/wp-sudo-stream-bridge.php, mapping all 9 WP Sudo audit hooks into Stream records. Bridge remains inert when Stream APIs are unavailable and supports late plugin load order. - Feature: public integration API (
wp_sudo_check()/wp_sudo_require()) β added first-party helpers for third-party plugins/themes to require an active sudo session without registering full action rules.wp_sudo_require()can redirect to the challenge page in session-only mode (or returnfalsewhen redirecting is disabled/unavailable) and emitswp_sudo_action_gatedwith surfacepublic_api. - Docs: release alignment β updated developer reference and manual testing docs for Stream bridge and public API helpers; refreshed roadmap and contributing guidance for current development priorities and repo-local integration test paths.
- Pre-release hygiene β regenerated
bom.json. - 494 unit tests, 1286 assertions. 135 integration tests in CI.
- Docs release + metadata alignment β corrected post-v2.11.0 documentation drift: roadmap completion markers, RC re-test guidance, and release notes alignment across
CHANGELOG.md,readme.md, andreadme.txt. - Version annotation fixes β corrected
@sinceannotations introduced in the v2.11.0 development cycle so Phase 3/4 additions no longer reference the nonexistent2.10.3version. - Pre-release hygiene β regenerated
bom.jsonand updated ignore rules to keep.planning/private-reference/,.composer_cache/, andvendor_test/out of commits. - 478 unit tests, 1228 assertions. 130 integration tests in CI.
- Phase 3 complete: Action Registry schema validation hardening β filtered
wp_sudo_gated_actionsrules are now normalized and validated before caching, preventing malformed third-party payloads from reaching gate matchers. - Phase 3 complete: MU-loader resilience β loader basename/path resolution now follows an explicit fallback chain and correctly respects active plugin state in single-site and multisite environments.
- Phase 4 complete: WPGraphQL persisted-query strategy β GraphQL policy behavior was tightened and documented for persisted-query/headless setups, with expanded integration coverage of mutation classification and bypass behavior.
- Phase 4 complete: WSAL sensor bridge β added
bridges/wp-sudo-wsal-sensor.php, mapping all 9 WP Sudo audit hooks to WP Activity Log events for security telemetry integration. - Housekeeping: Admin bar class cleanup β docblock trimming, explicit
$accepted_argson hook registrations, no behavioral changes. - Docs and planning closure β phase summaries and roadmap/planning artifacts updated to reflect completion across Phases 1β4 of the security hardening sprint.
- 478 unit tests, 1228 assertions. 130 integration tests in CI.
- Fix: multisite uninstall orphaned MU-plugin shim and user meta β when a network-activated plugin was uninstalled, the early-return path skipped
wp_sudo_cleanup_mu_shim()andwp_sudo_cleanup_user_meta(), leaving the shim file and session metadata in the database after plugin deletion. Multisite uninstall now unconditionally cleans all sites and all network-wide data. - Fix:
wp_sudo_versionoption not deleted on uninstall βwp_sudo_cleanup_site()deleted four options but missedwp_sudo_version, leaving an orphan row. Also added the missingdelete_site_option( 'wp_sudo_role_version' )to the multisite network cleanup path. - Fix:
Admin::get()TypeError on PHP 8.2+ with corrupted settings βget_option()returningfalse(from corrupted serialized data) was assigned to a?arraytyped property, causing a TypeError. Now validates the return withis_array()and falls back to defaults. - Fix:
Gate::matches_rest()crash on invalid third-party regex β third-party filters onwp_sudo_gated_actionscould inject rules with malformed regex patterns, causingpreg_match()warnings. Newsafe_preg_match()wrapper catches the warning and fails closed (rule does not match). - Psalm 6.15.1 static analysis β added alongside PHPStan for dual static analysis. Psalm surfaced the
absint()β(int)cast fix in Admin and thewp_safe_redirect()fallback in Gate (both shipped in v2.10.0 but caught by Psalm). Type coverage published to Shepherd.dev on default-branch pushes. - Codecov integration β unit test coverage uploaded to Codecov on CI runs.
- 16 new unit tests closing gaps in CLI cron-policy enforcement, network activation lifecycle, network admin settings save, admin bar deactivation handler, transient storage failures, cookie/token edge cases, and 2FA provider availability.
- Dependency bumps β PHPStan 2.1.40, Yoast PHPUnit Polyfills 4.0.0, actions/checkout v6, actions/cache v5, actions/upload-artifact v7, actions/github-script v8.
- CodeQL and Dependabot β CodeQL JavaScript security scanning enabled; Dependabot version updates for Composer and GitHub Actions.
- 428 unit tests, 1043 assertions. 92 integration tests in CI.
- Fix: accessibility audit follow-up β admin bar countdown polish, docs alignment.
- 397 unit tests, 944 assertions. 92 integration tests in CI.
- Feature: WebAuthn gating bridge β gates WebAuthn key registration and deletion via
wp_sudo_gated_actionsfilter when the Two Factor WebAuthn plugin is active. - Fix: WP 7.0 notice CSS β corrected admin notice styling for WordPress 7.0 compatibility.
- Fix: MU-plugin shim respects deactivation β the loader now checks
active_plugins/active_sitewide_pluginsbefore loading the plugin; inert when deactivated. - Fix: localize app-password JS β moved inline script to localized data; paginate stale sessions in Site Health; fix return_url handling.
- Fix: clamp 2FA window filter β
wp_sudo_two_factor_windowfilter output clamped to documented 1β15 minute bounds. - REST
_wpnoncefallback β Gate accepts_wpnoncequery parameter for REST authentication when cookie nonce header is absent. - Exit path integration tests β new test suite for security-critical exit paths (REST 403, AJAX 403, admin redirect, challenge auth, grace window).
- PCOV coverage CI job β unit test coverage generation added to CI pipeline.
- Docs: NIST SP 800-63B terminology alignment β reauthentication language updated throughout.
- 397 unit tests, 944 assertions. 92 integration tests in CI.
- Fix: 2FA help text corrected β
includes/class-admin.phpdisplayed "The default 2FA window is 10 minutes" but the code default (set in v2.4.0) is5 * MINUTE_IN_SECONDS. Help text now reads "5 minutes". The sudo session countdown (admin bar) is a separate, unrelated timer that remains at 15 minutes. - Fix: version constant drift in dev bootstrap files β
phpstan-bootstrap.phpandtests/bootstrap.phpboth definedWP_SUDO_VERSION = '2.8.0'while the runtime plugin was at v2.9.1. Both bumped to'2.9.2'. - Docs: readme.txt expanded β Patchstack 2026 attack statistics (57% BAC, 80% sudo-mitigated, 5 h median exploit time) added to the Description section. Eight new FAQ entries added: what problem Sudo solves, how it differs from security plugins, limitations, brute-force protection, login session grant, password change behaviour, grace period, and the 2FA verification window. Integration and unit test counts corrected.
- 397 unit tests, 944 assertions.
- Docs: threat model kill chain β
docs/security-model.mdgains a new "Threat Model: The Kill Chain" section with verified statistics from Patchstack (2024 vulnerability breakdown), Sucuri (post-compromise forensics), Verizon DBIR (credential attacks), Wordfence (55B password attacks blocked), and OWASP Top 10:2025 (Broken Access Control #1). Risk reduction estimates table included.FAQ.mdadds a condensed "Why this matters by the numbers" paragraph. All statistics verified 2026-02-27 against primary sources. - Docs: project size table β
readme.mdgains a "Project Size" subsection (6,688 production lines, 11,555 test lines, 1.7:1 ratio).CLAUDE.mdgains verification commands and a pre-release checklist note to keep the table current. Stale test counts inreadme.mdcorrected (375/905 β 397/944, 73 β 92 integration). Missing v2.8.0 and v2.9.0 changelog entries added toreadme.md. - 397 unit tests, 944 assertions.
- Feature:
wp_sudo_action_allowedaudit hook β fires when a gated action is permitted by an Unrestricted policy. Covers all five non-interactive surfaces: REST App Passwords ($user_id, $rule_id, 'rest_app_password'), WP-CLI (0, $rule_id, 'cli'), Cron (0, $rule_id, 'cron'), XML-RPC (0, $rule_id, 'xmlrpc'), and WPGraphQL ($user_id, 'wpgraphql', 'wpgraphql'β mutations only). WPGraphQL queries do not fire the hook. Implemented by adding an'audit'mode toregister_function_hooks()for CLI/Cron/XML-RPC, and inlinedo_action()calls for REST and WPGraphQL. This is the ninth audit hook. - Docs: CLAUDE.md accuracy audit β corrected six inaccuracies: policy names, missing doc reference, missing password-change hooks in bootstrap sequence, rule count ambiguity, and hook count. Logged one confabulation (fabricated
wp_sudo_action_alloweddocumentation inai-agentic-guidance.md) inllm_lies_log.txt. - Docs: manual testing β MANUAL-TESTING.md adds Β§19 (Unrestricted audit hook verification for all five surfaces) with forward references from existing Unrestricted subsections.
- 397 unit tests, 944 assertions.
- Feature: expire sudo session on password change β hooks
after_password_reset(lost-password flow) andprofile_update(admin profile, user-edit, REST API) to invalidate any active sudo session when a user's password changes. Handlers guard with a meta-existence check before callingdeactivate()to avoid phantomwp_sudo_deactivatedaudit events for users without sessions. Closes the gap where a compromised session persisted after a password reset. - Feature: WPGraphQL conditional display β the WPGraphQL policy dropdown on Settings > Sudo, the WPGraphQL paragraph in the "Session & Policies" help tab, and the Site Health policy review all adapt based on whether WPGraphQL is installed. When inactive: the dropdown is hidden, the help tab shows an install note instead of the full explanation, and Site Health does not flag the WPGraphQL policy.
- Docs: WPGraphQL surface-level gating rationale β
docs/developer-reference.mdRule Structure section now explains why WPGraphQL is gated at the surface level rather than per-action, with a forward reference to the WPGraphQL Surface section. - Docs: manual testing additions β MANUAL-TESTING.md adds Β§16.0 (WPGraphQL conditional behavior when plugin inactive) and Β§18 (password change expires sudo session β three scenarios).
- 391 unit tests, 929 assertions.
- Feature:
wp_sudo_wpgraphql_bypassfilter β fires in Limited mode before mutation detection. Returntrueto allow a request through without sudo session checks. Solves compatibility with wp-graphql-jwt-authentication: the JWTloginmutation is sent by unauthenticated users and was blocked by the default Limited policy, breaking the entire JWT authentication flow. A documented bridge mu-plugin exemptsloginandrefreshJwtAuthTokenmutations while keeping all other mutations gated. The filter does not fire in Disabled or Unrestricted mode. - Fix: WPGraphQL now listed in non-interactive entry points β the "How Sudo Works" help tab text omitted WPGraphQL from the list of non-interactive surfaces.
- 379 unit tests, 915 assertions.
- Fix: WPGraphQL integration tests now call
check_wpgraphql()directly βGate::check_wpgraphql( string $body ): ?WP_Errorextracted fromgate_wpgraphql()so integration tests can exercise the policy logic without WPGraphQL installed orwp_send_json()/exitside effects. No behavioral change in production. Fixes a pre-existing CI regression introduced in v2.5.1 when WPGraphQL gating moved fromrest_request_before_callbackstographql_process_http_request. - Docs: full documentation update for v2.6.0 β FAQ, ROADMAP, readme.txt, readme.md, developer-reference.md, security-model.md, MANUAL-TESTING.md, CLAUDE.md updated to reflect all v2.6.0 features (login grant, password-change gating, grace period).
- 375 unit tests, 905 assertions. 73 integration tests in CI.
- Feature: login implicitly grants a sudo session β a successful WordPress browser-based login (via
wp_login) now automatically activates a sudo session. The user just proved their identity via the login form; requiring a second challenge immediately is unnecessary friction. This mirrors the behaviour of Unixsudoand GitHub's sudo mode. Application Password and XML-RPC logins are unaffected (wp_logindoes not fire for those). Implemented inPlugin::grant_session_on_login(). - Feature:
user.change_passwordgated action β changing a user's password onprofile.phporuser-edit.phpnow requires an active sudo session. The rule fires only whenpass1orpass2is present in the POST body, narrowing it to actual password changes (not bio, email, or role updates, which also useaction=update). The REST counterpart gates anyPUT/PATCHto/wp/v2/users/{id}or/wp/v2/users/methat includes apasswordparameter. Closes the "session theft β silent password change β lockout" attack chain. - Feature: grace period (two-tier expiry) β sudo sessions now have a 120-second grace window (
GRACE_SECONDS = 120) after they expire. If a user was filling in a form while the session expired, the form submission still passes the gate without requiring re-authentication. The grace window is session-token-verified β a stolen cookie in a different browser does not gain grace access. Session meta cleanup is deferred until the grace window closes so the token is available for verification throughout. The four admin-bar UI call sites are intentionally excluded (the timer always reflects the true session state). - 375 unit tests, 905 assertions. 73 integration tests in CI.
- Fix: WPGraphQL Limited policy now blocks unauthenticated mutations β cross-origin requests (e.g. from a SvelteKit frontend) do not carry WordPress session cookies, so
get_current_user_id()returns 0. Previously the Limited policy silently passed these through via anif (!$user_id) returnguard before the session check was reached. Now, unauthenticated mutations are blocked with the samesudo_blocked403 response as authenticated-without-session mutations. The$user_id &&short-circuit preventsSudo_Session::is_active()from ever being called with user 0. - Fix: per-application-password policy dropdown now renders on profile pages β two bugs prevented the dropdown column from appearing: (1) the JS looked for
.application-passwords-list-tablewhich does not exist; the table lives inside.application-passwords-list-table-wrapper. (2) UUID extraction tried to readdata-slugfrom the revoke button, which carries no data attributes; WordPress already setsdata-uuiddirectly on each<tr>. Both are now corrected. - Fix:
user.promoterule now fires on bulk role changes β the Action Registryuser.promoterule declared'method' => 'GET', but WordPress's bulk role change action onusers.phpPOSTs the request. Changing the method toANYensures both the single-user edit and bulk promote paths are gated. - Security: sudo session required for MU-plugin install/uninstall β
handle_mu_install()andhandle_mu_uninstall()only checked capability; now they also require an active sudo session (returnssudo_required403 if absent). - Security: sudo session required for per-app-password policy save β
handle_app_password_policy_save()only checked capability; now also requires an active sudo session. - UX: per-app-password dropdown surfaces
sudo_requiredresponse β when the policy save AJAX returnssudo_required, the dropdown restores its previous value, shows an amber outline, and displays an alert explaining that a sudo session is needed. - Docs: WPGraphQL persisted queries caveat β corrected the absolute claim in
security-model.mdthat the mutation heuristic "cannot false-negative on an actual GraphQL mutation"; it cannot detect mutations sent via the Persisted Queries extension. Added explicit guidance to use the Disabled policy in persisted-query environments. - Docs: WPGraphQL headless authentication boundary β added a new subsection to
security-model.mdexplaining why Limited behaves identically to Disabled for most cross-origin headless deployments, with per-deployment policy recommendations. Cross-referenced from a new## WPGraphQL Surfacesection indocs/developer-reference.md. - 361 unit tests, 882 assertions. 73 integration tests in CI.
- Fix: WPGraphQL gating now functional β v2.5.0 hooked into
rest_request_before_callbacks, but WPGraphQL dispatches requests via WordPress rewrite rules atparse_request, not through the REST API pipeline. The REST filter never fired for GraphQL requests. The fix hooks into WPGraphQL's owngraphql_process_http_requestaction, which fires after authentication but before body reading, regardless of how the endpoint is named or configured. No endpoint URL matching is needed. - Remove
wp_sudo_wpgraphql_routefilter β the filter was designed for the now-dead URL-matching approach and has no effect. Removed from the codebase and all documentation. - 361 unit tests, 881 assertions. 73 integration tests in CI.
- WPGraphQL surface gating β adds WPGraphQL as a fifth non-interactive surface alongside WP-CLI, Cron, XML-RPC, and Application Passwords. Three-tier policy (Disabled / Limited / Unrestricted); default is Limited. In Limited mode, GraphQL mutations are blocked without an active sudo session while read-only queries pass through. Fires the
wp_sudo_action_blockedaudit hook on block. The policy setting (wpgraphql_policy) is stored regardless of whether WPGraphQL is installed; the settings field is only shown when WPGraphQL is active. - Mutation detection heuristic β Limited mode checks whether the POST body contains the word
mutation. Intentionally blunt: cannot false-negative on actual mutations, may false-positive on queries mentioning "mutation" in a string argument. Documented indocs/security-model.md. wp_sudo_wpgraphql_routefilter β allows the gated route to be overridden to match custom WPGraphQL endpoint configurations.- Site Health integration β WPGraphQL policy included in the Entry Point Policies health check (flagged if set to Unrestricted).
- 364 unit tests, 887 assertions. 73 integration tests in CI.
- Documentation: roadmap consolidation β Merged three separate roadmaps (
ROADMAP.md,ACCESSIBILITY-ROADMAP.md,docs/roadmap-2026-02.md) into one unifiedROADMAP.mdat project root. MovedCHANGELOG.mdandFAQ.mdto root for prominence. - Planned Development Timeline β Added comprehensive timeline at the top of ROADMAP.md showing immediate, short-term, medium-term, and deferred work phases. Provides quick reference for what will actually be implemented.
- Table of Contents β Added scannable TOC to ROADMAP.md linking to all 10 sections plus appendix.
- AJAX gating integration tests β 11 new tests covering the AJAX surface: rule matching for all 7 declared AJAX actions, full intercept flow via
wp_doing_ajaxfilter, session bypass, non-gated pass-through, blocked transient lifecycle, admin notice fallback (render_blocked_notice), andwp.updatesslug passthrough. - Action registry filter integration tests β 3 new tests verifying custom rules added via
wp_sudo_gated_actionsare matched by the Gate in a real WordPress environment; including custom admin rules, custom AJAX rules, and filter-based removal of built-in rules. - Audit hook coverage β
wp_sudo_action_blockednow integration-tested for CLI, Cron, and XML-RPC surfaces (in addition to REST app-password). Documents thatwp_sudo_action_allowedis intentionally absent from the production code path. - CI quality gate β new GitHub Actions job runs PHPCS and PHPStan on every push and PR; Composer dependency cache added to unit and integration jobs; nightly scheduled run at 3 AM UTC catches WordPress trunk regressions.
- MU-plugin manual install instructions β fallback copy instructions added to the settings page UI (
<details>disclosure) and help tab for environments where the one-click installer fails due to file permissions. - CONTRIBUTING.md β new contributor guide covering prerequisites, local setup, unit vs integration test distinction, TDD workflow, and lint/analyse requirements.
- 349 unit tests, 863 assertions. 73 integration tests in CI.
- Integration test suite β 55 integration tests running against a real WordPress + MySQL environment via
WP_UnitTestCase. Covers sudo session lifecycle (bcrypt verification, token binding, rate limiting, expiry), request stash/replay with transient TTL, full reauth flow (5-class end-to-end), REST API gating with cookie auth and application passwords, upgrader migration chain, audit hook arguments, Two Factor plugin interaction, and multisite session isolation. - CI pipeline β GitHub Actions workflow with unit tests across PHP 8.1β8.4 and integration tests against WordPress
latestandtrunk(including multisite variant). MySQL 8.0 service container with health checks. - Fix: multisite site-management gate gap β Archive, Spam, Delete, and Deactivate site actions on Network Admin β Sites now correctly trigger the sudo challenge. WordPress core's
sites.phpsendsaction=confirmwith the real action inaction2; the Gate now checks both parameters. - Fix: admin bar timer width β the countdown timer's red (expiring) state no longer stretches wider than the green (active) state. Defensive CSS constrains the background to content width regardless of WP core layout context.
- Fix: WP 7.0 admin notice background β restored white background on WP Sudo admin notices, which lost their background color in WP 7.0's admin visual refresh.
- Fix: 2FA countdown advisory-only β the two-factor verification window is now advisory (5 minutes, reduced from 10). Expired 2FA codes are still accepted if the underlying provider validates them, preventing false rejections for slow email delivery.
- Fix:
setcookie()headers-already-sent guard βSudo_Session::activate()now checksheaders_sent()before callingsetcookie(), preventing warnings in CLI and integration test contexts. - Verification requirements β CLAUDE.md now mandates live source verification for all external code references, with documented verification commands. LLM lies log tracks 5 prior fabrications that were corrected.
- WP 7.0 Beta 1 tested β manual testing guide completed against WP 7.0 Beta 1 (15 sections, all PASS). Visual compatibility, help tabs, challenge page, and admin bar verified against the refreshed admin chrome.
- 349 unit tests, 863 assertions. 55 integration tests in CI.
- Fix: admin bar sr-only text leak β screen-reader-only milestone text no longer renders in the dashboard canvas. The admin bar
<li>node now establishes a containing block (position: relative) and sr-only elements useclip-path: inset(50%)alongside the legacyclipproperty. - Documentation overhaul β readmes slimmed to storefront length. Full content extracted to
docs/: security model, developer reference, FAQ, and this changelog. Manual testing guide rewritten for v2.3.1+ with per-app-password testing, MU-plugin toggle, and iframe edge case coverage. - Composer lock compatibility β
config.platform.phpset to8.1.99so the lock file resolves packages compatible with PHP 8.1+ regardless of the local PHP version. Fixes Copilot coding agent CI failure (doctrine/instantiator2.1.0 requiring PHP 8.4+). - Housekeeping β removed stale
WP-SUDO-PROJECT-STATE.md; added@since 2.0.0to Upgrader class; updated CLAUDE.md and.github/copilot-instructions.mdwith docs/ file listings. - 343 unit tests, 853 assertions.
- Fix: Unicode escape rendering β localized JS strings using bare
\uXXXXescapes (not valid PHP Unicode syntax) now use actual UTF-8 characters, fixing visible backslash-escape text during challenge replay. - Fix: screen-reader-only text flash β the sr-only "Verifying..." span no longer flashes visible fragments inside the flex container during challenge replay.
- CycloneDX SBOM β
bom.jsonshipped in the repo for supply chain transparency. Regenerate withcomposer sbom. - Help tabs β per-application-password policy section added to the Settings help tab. Help tab count corrected from 4 to 8 across readmes.
- Copilot coding agent β
.github/copilot-instructions.mdandcopilot-setup-steps.ymladded for GitHub Copilot integration. - Accessibility roadmap complete β all items (critical through low priority) verified resolved and documented.
- 343 unit tests, 853 assertions.
- Per-application-password sudo policies β individual Application Password credentials can now have their own Disabled, Limited, or Unrestricted policy override, independent of the global REST API (App Passwords) policy. Configure per-password policies from the Application Passwords section on the user profile page.
- Challenge page iframe fix β the reauthentication challenge page now breaks out of WordPress's
wp_iframe()context, fixing a nested-frame display issue during plugin and theme updates. - Accessibility improvements β admin bar countdown timer cleans up on page unload; lockout countdown screen reader announcements throttled to 30-second intervals; settings fields display default values.
- PHPStan level 6 static analysis β full codebase passes PHPStan level 6 with zero errors.
- Documentation β new AI and agentic tool guidance and UI/UX testing prompts.
- 343 unit tests, 853 assertions.
- Security hardening β stashed redirect URLs are now validated with
wp_validate_redirect()before replay. - Accessibility β ARIA
role="alert"androle="status"added to gate notices; disabled-action text color improved to 4.6:1 contrast ratio (WCAG AA). - 2FA ecosystem documentation β new integration guide and ecosystem survey covering 7 major 2FA plugins with bridge patterns.
- WP 2FA bridge β drop-in bridge for WP 2FA by Melapress supporting TOTP, email OTP, and backup codes (
bridges/wp-sudo-wp2fa-bridge.php). - Help tabs β Settings tab moved to 2nd position; all four 2FA hooks documented; Security Model heading added.
- 334 unit tests, 792 assertions.
- Three-tier entry point policies β replaces the binary Block/Allow toggle with three modes per surface: Disabled (shuts off the protocol entirely), Limited (default β gated actions blocked, non-gated work proceeds normally), and Unrestricted (everything passes through).
- Function-level gating for non-interactive surfaces β WP-CLI, Cron, and XML-RPC now hook into WordPress function-level actions (
activate_plugin,delete_plugin,set_user_role, etc.) instead of matching request parameters. This makes gating reliable regardless of how the operation is triggered. - CLI enforces Cron policy β
wp cronsubcommands respect the Cron policy even when CLI is Limited or Unrestricted. If Cron is Disabled,wp cron event runis blocked. - REST API policy split β Disabled returns
sudo_disabled(surface is off), Limited returnssudo_blocked(gated action denied), clearly distinguishing the two rejection reasons. - Automatic upgrade migration β existing
blocksettings migrate tolimited,allowtounrestricted. Multisite-aware. - Site Health updated β Disabled is treated as valid hardening (Good status). Unrestricted triggers a Recommended notice.
- Manual testing guide β comprehensive step-by-step verification procedures in
tests/MANUAL-TESTING.md. - 327 unit tests, 752 assertions.
- Removes the
unfiltered_htmlcapability from the Editor role. Editors can no longer embed scripts, iframes, or other non-whitelisted HTML β KSES sanitization is always active for editors. Administrators retainunfiltered_html. The capability is restored if the plugin is deactivated or uninstalled. - Adds tamper detection: if
unfiltered_htmlreappears on the Editor role (e.g. via database modification), it is stripped automatically and thewp_sudo_capability_tamperedaction fires for audit logging. - Fixes admin bar deactivation redirect: clicking the countdown timer to end a session now keeps you on the current page instead of redirecting to the dashboard.
- Replaces WordPress core's confusing "user editing capabilities" error with a clearer message when a bulk role change skips the current user.
Complete rewrite. Action-gated reauthentication replaces role-based privilege escalation.
- New model β gates dangerous operations behind reauthentication for any user, regardless of role. No custom role, no capability escalation.
- Full attack surface coverage β admin UI (stash-challenge-replay), AJAX (error + admin notice + session activation), REST API (cookie-auth challenge, app-password policy), WP-CLI, Cron, XML-RPC.
- Action Registry β 20 gated rules across 7 categories (plugins, themes, users, editors, options, updates, tools), plus 8 multisite-specific rules. Extensible via
wp_sudo_gated_actionsfilter. - Entry point policies β three-tier Disabled/Limited/Unrestricted policies for REST Application Passwords, WP-CLI, Cron, and XML-RPC.
- 2FA browser binding β challenge cookie prevents cross-browser 2FA replay.
- 2FA countdown timer β visible countdown during the verification step; configurable window via
wp_sudo_two_factor_windowfilter. - Self-protection β WP Sudo settings changes are gated.
- MU-plugin toggle β one-click install/uninstall from the settings page. Stable shim + loader pattern keeps the mu-plugin up to date with regular plugin updates.
- Multisite β network-wide settings, network-wide sessions, 8 network admin rules,
get_site_option/set_site_transientstorage. - 8 audit hooks β full lifecycle and policy logging for integration with WP Activity Log, Stream, and similar plugins.
- Contextual help β 8 help tabs on the settings page.
- Accessibility β WCAG 2.1 AA throughout (ARIA labels, focus management, status announcements, keyboard support).
- 281 unit tests, 686 assertions.
- In-place modal reauthentication; no full-page redirect.
- AJAX activation, accessibility improvements, expanded test suite.
- M:SS countdown timer, red bar at 60 seconds, accessibility improvements.
- Multisite-safe uninstall, contextual Help tab.
- 15-minute session cap, two-factor authentication support,
unfiltered_htmlrestriction.
- Initial release.