Repository navigation
Serve Composer routes for reason-dev packages - #5
Conversation
Under Bref/API Gateway SCRIPT_NAME isn't set, so the parent guessServerUrl() fell back to "/" and the metadata's download_url became relative. WordPress rejects that with "Download failed. A valid URL was not provided." - Build the URL from the Host header (or SERVER_NAME) and the domain root, honoring X-Forwarded-Proto since API Gateway terminates TLS. - Call the Lambda subclass's guessServerUrl() from index.php; it was calling the base class, so the override never ran. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…otation Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…ify the runbook Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review from the consumer side (wp-site-template, reason-edge-delivery), which now treat this branch as the canonical distribution mechanism. Recommendations: document the p2 publish contract before plan 2 implements it (name/type/installer-name/require derived from each plugin's composer.json; dist zips without vendor/), mark the not-yet-existing rebuild-composer-index.sh runbook pointer as pending, promote dual-key rotation to a follow-up, and open the PR this branch's plan calls for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cQuwZuNAe6vwnnnMGbTfU
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it introduces a new authenticated routing layer (Bearer-key auth, S3-backed Composer metadata/dist responses with presigned URLs) ahead of the existing update server, a human look would still be worthwhile.
What was reviewed: the Bearer-token check and fail-closed 503/401 behavior in Wpup_ComposerRepository::isAuthorized/handle; slug/version/path-traversal validation before any S3 call; the reserved-prefix routing split between composer-routing.php and the legacy Wpup_LambdaS3UpdateServer path; and S3 error handling in serveP2/serveDist (404 vs 500 mapping, with createPresignedRequest left outside the try/catch that wraps headObject).
Extended reasoning...
The diff adds a new Composer v2 repository (ComposerRepository/ComposerResponse), wires it in ahead of the existing plugin-update routing, reworks guessServerUrl() for absolute download URLs, and adds CI plus unit/acceptance tests; it is a large (~1700 line) change touching authentication (Bearer SIMPLE_UPDATE_KEY), S3 access, and presigned URL generation for premium plugin distribution. No findings were produced by the bug hunt (exit reason dry_streak, i.e. it ran dry rather than being cut off), and three candidate issues (legacy plugin slugs colliding with reserved Composer path segments, and an unwrapped createPresignedRequest call in serveDist) were already investigated and ruled out/flagged in the supplied analysis. Given the security-sensitive auth/S3 surface and overall size of the change, a human review is still warranted even though automated review found nothing to block on.
The checkboxes were never ticked as work landed, so the plan read as unstarted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
packages.reason.com now also works as a read-only Composer repository for
reason-dev/*packages. It serves/packages.json,/p2/reason-dev/<slug>.jsonand/dist/reason-dev/<slug>/<slug>-<version>.zip, all behindSIMPLE_UPDATE_KEYsent as a Bearer header. The existing update-checker route that legacy WordPress sites use is unchanged.Why
No Asana task.
vnext sites install Reason plugins with Composer, so they need a Composer repository.
wp-site-template(docs/plugin-distribution.md) andreason-edge-deliveryalready document this branch as the canonical distribution mechanism (seedocs/plans/2026-10-06-composer-routes-review.md). The longer it sits unmerged, the more those downstream docs can drift from what is deployed.Changes
includes/Wpup/ComposerRepository.phpadds a router with no side effects. It takes a path and headers and returns a response, ornullfor paths it doesn't own. It only reads S3.reason-devvendor, and never servesmeta/.Authorization: Bearer(the scheme name is case-insensitive) and never from?key=.503, so a stage with no key is never an open repository.404, before making any S3 call.302) to a presigned S3 URL that expires after 15 minutes.500with a log line, not404.includes/Wpup/ComposerResponse.phpadds the response value object.includes/composer-routing.phpandindex.phptry the Composer routes first. OtherwiseWpup_Requestwould readp2,distand so on as plugin slugs. Every other request falls through toWpup_LambdaS3UpdateServerexactly as before.index.phpkeeps callingWpup_LambdaS3UpdateServer::guessServerUrl().loader.phploads the two new classes.serverless.yml:SIMPLE_UPDATE_KEYcomment: set the key withaws ssm put-parameterat the exact path the deploy reads, becausebref secret:createdoesn't document where it writes.tests/check.php,tests/composer-repository-test.phpandtests/index-routing-test.phpare plain-PHP tests. They use the AWS SDK'sMockHandler, so they need no S3 access..github/workflows/tests.ymlruns the tests on PHP 8.4 for every pull request and every push to master.tests/acceptance/composer-smoke.shis an end-to-end smoke test for a deployed stage. It uploads a throwawayreason-dev/wpup-smokepackage, runscomposer installwith Bearer auth, checks the lockfile, and checks that a key sent in the query string is refused.docs/runbook.mdcovers the routes, the append-only storage rule (and its one exception, the smoke package), and how to rotate the key.docs/plans/holds the implementation plan and the downstream review notes.Also in this PR: two commits that were on local
masterbut never pushed.a9a456cis acomposer update.555d9c7builds an absolutedownload_url(without it, WordPress fails updates with "A valid URL was not provided"). This branch builds on555d9c7.Alternatives considered
?key=on Composer routes, as the update-checker route does. Rejected: a key in a URL ends up incomposer.lockfiles and access logs.503is safer.Wpup_LambdaS3UpdateServer. Rejected: it would change the class that legacy sites depend on. A separate router that returnsnullfor paths it doesn't own leaves that class untouched.Testing
php tests/composer-repository-test.php: 37 checks, ALL PASSED (local PHP 8.5.11).php tests/index-routing-test.php: 5 checks, ALL PASSED.curl https://packages.reason.com/packages.jsonreturns400today. Production is still running master, which readspackages.jsonas a plugin slug.Not run as part of this PR:
dev, runcomposer-smoke.sh, confirm the update-checkerdownload_urlis absolute. I couldn't confirm whether they ran, so treat them as outstanding.Not in this PR
/packages.jsonshould return503until the key is enforced, or401once it is.SIMPLE_UPDATE_KEYin production: the plugin fleet pins awp-update-checkerthat predates key support. Re-lock and release the fleet first, and run the plan's Task 0 pre-flight check on each legacy site. If a site's first-loaded checker predates key support, it sends no key and loses updates for every Reason plugin.name,type: wordpress-plugin,extra.installer-name, arequirelist taken from the plugin's owncomposer.json, and dist zips with novendor/. Do this before plan 2 builds the publish pipeline.scripts/rebuild-composer-index.shin the runbook as "lands with plan 2". It doesn't exist inreason-wp-repo-actionsyet.SIMPLE_UPDATE_KEY_PREVIOUS) during rotation, so rotating the key doesn't break the fleet for a window.p2/,dist/andmeta/) is plan 2, inreason-wp-repo-actions.🤖 Generated with Claude Code