Skip to content

fix(core): grant plugin media write access on storage-only backends (#1313)#1562

Open
marcusbellamyshaw-cell wants to merge 1 commit into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/media-write-r2-binding-1313
Open

fix(core): grant plugin media write access on storage-only backends (#1313)#1562
marcusbellamyshaw-cell wants to merge 1 commit into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/media-write-r2-binding-1313

Conversation

@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes ctx.media.upload() and ctx.media.delete() being silently unavailable to in-process (native) plugins on storage backends that don't support presigned URLs — notably the Cloudflare R2 Worker binding.

In PluginContextFactory.createContext, media write access was gated on this.getUploadUrl being truthy:

if (capabilities.has("media:write") && this.getUploadUrl) {
  media = createMediaAccessWithWrite(this.db, this.getUploadUrl, this.storage);
}

But upload() never uses getUploadUrl — it only needs storage. The R2 Worker binding intentionally provides storage but no presigned-URL support, so getUploadUrl is never set and a plugin granted media:write loses all write access despite having a fully functional storage backend. The only workaround today is to bypass the media API and write to the bucket binding directly.

This is the same gap the @emdash-cms/cloudflare sandbox wrapper already papers over by setting getUploadUrl to a stub that throws — confirming the intent is for upload() to work on the binding.

The fix

  • Gate media write access on (this.getUploadUrl || this.storage), so a plugin can write whenever it actually can — via a presign provider or a direct storage backend.
  • Make getUploadUrlFn optional in createMediaAccessWithWrite. When no presign provider is configured, media.getUploadUrl() now throws a clear "not supported — use media.upload() instead" error rather than silently stripping upload()/delete() from the context.
  • No change to behavior when getUploadUrl is configured.

Closes #1313

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change) — packages/core plugin suites: 706 passed
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • I have added a changeset (if this PR changes a published package)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (Claude Code)

Screenshots / test output

New tests in packages/core/tests/integration/plugins/capabilities.test.ts (describe: "Media Access write gate (#1313)") cover:

  • media:write + storage (no getUploadUrl) → upload()/delete() available (the regression)
  • upload() round-trips bytes through the storage backend
  • getUploadUrl() rejects with a clear error when no presign provider exists
  • existing getUploadUrl-configured path still grants write
  • media:read stays read-only; media:write with neither storage nor presign nor read → no media access
Test Files  30 passed (30)
     Tests  706 passed (706)

…mdash-cms#1313)

ctx.media.upload() and ctx.media.delete() were gated behind getUploadUrl
being set, but upload() only needs a storage backend. On the Cloudflare R2
Worker binding (which has no presigned-URL support) this stripped write access
from media:write plugins entirely, forcing them to bypass the media API.

Gate media write access on (getUploadUrl OR storage). getUploadUrl() now
throws a clear "not supported" error when no presign provider is configured,
instead of silently removing upload()/delete() from the context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 83ae344

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@emdashbot

emdashbot Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Could not push formatting changes to this fork. The contributor may have "Allow edits by maintainers" disabled.

Please run the formatter locally:

pnpm format

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ctx.media.upload() unavailable for in-process plugins when using R2 Worker binding

1 participant