Skip to content

Leave the Composer routes open while SIMPLE_UPDATE_KEY is unset - #6

Merged
justinmaurerdotdev merged 1 commit into
masterfrom
feat/composer-routes-ungated-without-key
Oct 9, 2026
Merged

justinmaurerdotdev merged 1 commit into
masterfrom
feat/composer-routes-ungated-without-key

Conversation

@justinmaurerdotdev

Copy link
Copy Markdown
Member

Summary

The Composer routes now follow the update-checker route's rule: open while SIMPLE_UPDATE_KEY is unset, bearer-gated once it is set. They used to answer 503 without a key.

Why

No Asana task. One key gates both routes, and setting it in production makes the legacy update route require it too, which needs every legacy plugin to bundle a key-aware update checker first (a composer update across the plugin repos that hasn't happened). With the 503, no Composer-built site could install from production until that fleet-wide work was done. This decouples the two: the first template site can build against production now, and the key goes on when the legacy fleet is ready.

Changes

  • includes/Wpup/ComposerRepository.php: drop the 503 branch; the 401 check runs only when a key is configured.
  • tests/composer-repository-test.php: replace the 503 test with open-mode tests (no header 200, stray bearer ignored, p2 served, unknown path still 404). The 401 tests with a key set are unchanged.
  • serverless.yml, docs/runbook.md: comment and route table updated.

Alternatives considered

  • A second variable or a Composer-only key, so the legacy route could stay open while Composer is gated: more moving parts and a second secret to rotate; the one-key rule is the simpler state to reason about.

Testing

php -l includes/Wpup/ComposerRepository.php      ok
php tests/composer-repository-test.php           ALL PASSED
php tests/index-routing-test.php                 ALL PASSED
negative control: with the router change reverted, 4 of the new checks fail

Not run: tests/acceptance/composer-smoke.sh (it asserts 401s, so it only applies to a stage with the key set, where behavior is unchanged) and a deploy.

Not in this PR

  • Deploy to prod (bref deploy --env prod) after merge. The prod key stays unset.
  • While the key is unset, the Composer dist zips are reachable the same way the legacy packages/ zips already are: via a short-lived presigned URL handed out without auth. Same exposure as today's legacy route, now stated in the router docblock.
  • wp-site-template docs/plugin-distribution.md still says prod Composer routes answer 503; fix it in PR Bump guzzlehttp/guzzle from 7.10.5 to 7.15.5 #3's rebase.

🤖 Generated with Claude Code

https://claude.ai/code/session_015QYJYSm4FogA6Ds4TtgCZq

Matches the update-checker route, so a Composer-built site can install
from production before every legacy plugin bundles a key-aware checker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QYJYSm4FogA6Ds4TtgCZq
@justinmaurerdotdev
justinmaurerdotdev merged commit bf32322 into master Oct 9, 2026
2 checks passed
return Wpup_ComposerResponse::error(503, 'The Composer repository is disabled: SIMPLE_UPDATE_KEY is not set.');
}
if ( !$this->isAuthorized($headers) ) {
if ( $this->key !== '' && !$this->isAuthorized($headers) ) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Anyone can now download premium plugin zips from /dist/reason-dev/.../*.zip whenever SIMPLE_UPDATE_KEY is unset. The old code fail-closed (503) specifically to keep "premium plugin code" (base branch includes/Wpup/ComposerRepository.php:13-14) behind this route when no key was set; that line is deleted and the gate at line 56 now only checks isAuthorized when $this->key !== ''. Composer-only/vnext premium plugins that never shipped via the legacy route were never exposed unauthenticated before this change. Fix: keep dist (or all Composer routes) fail-closed, or require a Composer-specific key, until the specific vnext premium packages involved are confirmed safe to leave open; don't rely solely on URL-guessing difficulty as the safeguard.

Why this was flagged

Trigger: SIMPLE_UPDATE_KEY unset in an environment, which per the PR author is production's state immediately after merge and deploy. Entry point: GET /dist/reason-dev//-.zip, routed via wpup_route_composer()->handle() in includes/Wpup/ComposerRepository.php:50-60. At line 56 the 401 check now only runs when $this->key !== '', so with no key the request falls through to serveDist() (line 111), which issues a 302 to a presigned S3 URL with no authorization at all. On the base branch this path answered 503 specifically because, per the deleted docblock comment (base branch lines 13-14), "premium plugin code sits behind these routes." The PR calls this the same exposure as the legacy packages/ zips, but that only holds for plugins also served by the legacy route; plugins distributed only via the new Composer/vnext path were never reachable unauthenticated before this diff. No other safeguard limits this, since valid slugs are guessable or discoverable via /packages.json and /p2/..., which are also open.

Verification: Base includes/Wpup/ComposerRepository.php returned 503 when $this->key === '', per the docblock keeping "premium plugin code" behind these routes. The diff deletes that check; the merged gate at line 56 skips authorization when the key is unset, letting GET /dist/reason-dev//-.zip fall through to serveDist() (line 111), which returns a 302 to a presigned S3 URL with no authentication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant