Skip to content

feat(s3): reconcile browser-upload CORS on the app bucket during sync#92

Merged
stevethomas merged 1 commit into
mainfrom
steve/hopeful-proskuriakova-a8a011
Jun 5, 2026
Merged

feat(s3): reconcile browser-upload CORS on the app bucket during sync#92
stevethomas merged 1 commit into
mainfrom
steve/hopeful-proskuriakova-a8a011

Conversation

@stevethomas
Copy link
Copy Markdown
Member

Hey, I made a thing! 🥳

Great! Now please answer the following questions to help out your assigned reviewer:

What problems are you solving?

The app data bucket (bucket:, injected as AWS_BUCKET) is the target for direct browser→S3 presigned-PUT uploads — the pattern replacing Vapor's Vapor.store() (a hand-rolled SignedStorageUrlController issues a presigned putObject URL; the browser PUTs straight to S3). That cross-origin PUT triggers a CORS preflight, so the bucket must carry a CORS config. YOLO never set it — working apps relied on Vapor having configured it at create. convictrecords is migrating off Vapor and serves its own presigned-upload controller against its adopted bucket, so YOLO now owns the CORS to keep the upload path self-contained and survive a freshly-created bucket.

  • S3Bucket now implements SynchronisesConfigurationsynchroniseConfiguration() reconciles CORS to a permissive managed ruleset: origins *, methods GET/PUT/HEAD, AllowedHeaders *, MaxAgeSeconds 3600. ExposeHeaders is deliberately omitted (AWS drops empty optional fields on read, so an empty value would phantom-drift forever). create() applies CORS too.
  • BPA stays create-only — flipping Block Public Access under a live public bucket can break it, so it's never reconciled onto an existing bucket. Tags now reconcile on sync, so SyncS3BucketStep drops its bespoke no-op early-return and delegates to the standard syncResource() create-or-sync path (reconciles tags + CORS, never touches BPA).
  • Writes go direct via Aws::s3()->putBucketCors(...) — the S3 wrapper holds only read helpers, matching how AssetBucket/S3LoadBalancerLogs write.
  • Docs: manifest.md bucket section documents the reconciled CORS + tags and the create-only BPA.

Is there anything the reviewer needs to know to deploy this?

  • Ownership = overwrite. YOLO reconciles CORS on every sync. The permissive ruleset is close to Vapor's default, so the first sync on an adopted bucket (e.g. convictrecords) is a near-no-op — but any difference (the added HEAD / MaxAgeSeconds, or a hand-tuned config) is overwritten with the managed ruleset. The change is surfaced as a cors Change in the yolo sync plan / --check before apply.
  • CORS is not the access gate — it grants no data access. The presigned URL (auth + same-origin) is the gate; * origins just let the preflight pass (and keep the cutover canary fargate.<app> + bare-ALB origin working).
  • Not validated against real AWS yet. Unit fakes prove the documentsEqual drift logic but not S3's real CORS echo. Worth eyeballing the first convictrecords sync:app production --check when that cutover lands, to confirm the managed ruleset round-trips without perpetual drift.
  • Scoped to the per-env (production) bucket only — local-dev buckets and their *.test CORS are set separately. No POST / multipart methods (single presigned PUT only).
  • pint clean · 616 Pest pass (1490 assertions) · phpstan no errors.

🤖 Generated with Claude Code

The app data bucket (`bucket:`, injected as AWS_BUCKET) is the target for
direct browser→S3 presigned-PUT uploads — the pattern replacing Vapor's
Vapor.store(). That cross-origin PUT triggers a CORS preflight, so the bucket
must carry CORS. YOLO never set it; working apps relied on Vapor having
configured it at create. convictrecords is migrating off Vapor and serves its
own presigned-upload controller against its adopted bucket, so YOLO now owns
the CORS to keep the upload path self-contained.

- S3Bucket implements SynchronisesConfiguration: synchroniseConfiguration()
  reconciles CORS to a permissive managed ruleset (origins *, GET/PUT/HEAD,
  AllowedHeaders *, MaxAgeSeconds 3600; ExposeHeaders omitted to avoid
  empty-field phantom drift). create() now applies CORS too.
- BPA stays create-only (flipping it under a live public bucket can break it);
  tags now reconcile on sync, so the step delegates to the standard
  syncResource() create-or-sync path instead of its bespoke no-op early-return.
- Writes go direct via Aws::s3()->putBucketCors (the S3 wrapper holds only read
  helpers, matching AssetBucket/S3LoadBalancerLogs).
- Docs: manifest.md `bucket` section documents the reconciled CORS + tags and
  the create-only BPA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@stevethomas stevethomas merged commit 76fe2df into main Jun 5, 2026
5 checks passed
@stevethomas stevethomas deleted the steve/hopeful-proskuriakova-a8a011 branch June 5, 2026 04:38
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