Skip to content

feat(plugins): add sandbox media access capabilities - #3172

Merged
ascorbic merged 6 commits into
mainfrom
feat/sandbox-media-access
Sep 19, 2026
Merged

ascorbic merged 6 commits into
mainfrom
feat/sandbox-media-access

Conversation

@ascorbic

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds the independent sandboxed-plugin media-access vertical slice from the maintainer-provided capability expansion plan.

It keeps three authorities separate:

  • media:read returns expanded ready-media metadata without storage keys, author identity, content hashes, or bytes. Its asset URL uses an authenticated media-ID route, so logged-out requests stop before the route queries D1.
  • media:bytes:read adds ctx.media.readBytes(). Reads use the configured storage adapter, enforce the actual stream, default to 10 MiB, and cannot request more than 16 MiB. Content hashes are visible only with this capability.
  • media:metadata:write adds ctx.media.updateMetadata() for non-empty alt, caption, and complete focal-point patches. It cannot upload, replace, move, or delete media.

The capabilities round-trip through declaredAccess, shared/core/CLI manifest validation, the registry lexicon and generated types, install/update consent, native context, Cloudflare Worker Loader, Node/workerd, and the runtime-backed plugin test host. Public docs, canonical and generated authoring skills, and package changesets describe the same contract.

The mandatory independent adversarial review found and verified fixes for four issues before PR creation: storage-key leakage in adapter errors, a non-routable redacted URL, trusting stored size instead of the consumed stream, and empty metadata patches becoming reads. A second boundary review required and verified authenticated middleware/RBAC semantics for the opaque asset route. The final reviewed head is 74a469a32.

Upload, replacement, movement, and deletion changes are out of scope.

Prior feature approval: maintainer-directed implementation of the untracked sandbox capability expansion plan. The planning file is intentionally not included in this PR.

Closes # — not applicable.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: maintainer approval was supplied through the capability expansion plan and direct task instruction; there is no public Discussion URL.
  • I have included screenshots below if this PR changes the UI

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: GPT-5 Codex, with an independent adversarial-reviewer subagent

Screenshots / test output

Screenshots are not applicable. This PR adds localized labels to the existing consent-dialog component without changing its layout or styling. The rendered component test exercises the real update-escalation flow and verifies both new labels.

Validated at 74a469a32:

  • pnpm build
  • pnpm typecheck
  • pnpm lint
  • pnpm --dir docs build
  • core media/storage/auth/HTTP tests: 25 passed
  • plugin-types suite: 70 passed
  • plugin-CLI suite: 481 passed; focused generated-skill/manifest tests: 66 passed
  • plugin-test runtime host suite: 13 passed; focused Worker Loader media journey passed
  • workerd bridge-handler tests: 17 passed; focused real-workerd media journey passed
  • admin marketplace labels: 30 passed; consent-dialog component: 22 passed
  • registry lexicon generated-type tests: 21 passed

~ 🤖 Codex

@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fafcb80

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

This PR includes changesets to release 18 packages
Name Type
emdash Minor
@emdash-cms/plugin-types Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Minor
@emdash-cms/plugin-test Minor
@emdash-cms/plugin-cli Minor
@emdash-cms/registry-lexicons Minor
@emdash-cms/admin Minor
@emdash-cms/registry-verification Patch
@emdash-cms/registry-client Patch
@emdash-cms/registry-loader Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
create-emdash Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
@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

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,766 lines across 49 files. Large PRs are harder to review and more likely to be closed without review.
This PR spans 4 different areas (area/core, area/admin, area/docs, area/cloudflare). Consider breaking it into smaller, focused PRs.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Sep 17, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@pkg-pr-new

pkg-pr-new Bot commented Sep 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/admin@3172

@emdash-cms/auth

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth@3172

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth-atproto@3172

@emdash-cms/blocks

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/blocks@3172

@emdash-cms/cloudflare

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/cloudflare@3172

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/contentful-to-portable-text@3172

emdash

npm i https://pkg.pr.new/emdash-cms/emdash@3172

create-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/create-emdash@3172

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/gutenberg-to-portable-text@3172

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-cli@3172

@emdash-cms/plugin-test

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-test@3172

@emdash-cms/plugin-types

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-types@3172

@emdash-cms/registry-client

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-client@3172

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-lexicons@3172

@emdash-cms/registry-loader

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-loader@3172

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-moderation@3172

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@3172

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/sandbox-workerd@3172

@emdash-cms/x402

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/x402@3172

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-moderation@3172

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-atproto@3172

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-audit-log@3172

@emdash-cms/plugin-color

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-color@3172

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-embeds@3172

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-field-kit@3172

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-forms@3172

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-webhook-notifier@3172

commit: fafcb80

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://feat-sandbox-media-access.try.emdashcms.com, https://feat-sandbox-media-access-emdash-playground.emdash-cms.workers.dev (commit fafcb80)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://dc1f68ef.try.emdashcms.com, https://dc1f68ef-emdash-playground.emdash-cms.workers.dev fafcb80 2026-09-19T17:56:52.279Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://f65c48e9.try.emdashcms.com, https://f65c48e9-emdash-playground.emdash-cms.workers.dev d65daba 2026-09-19T17:22:45.260Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://3d5ee5b7.try.emdashcms.com, https://3d5ee5b7-emdash-playground.emdash-cms.workers.dev d294e7c 2026-09-19T07:33:37.355Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://0bf5a061.try.emdashcms.com, https://0bf5a061-emdash-playground.emdash-cms.workers.dev 044b22f 2026-09-19T06:59:28.170Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://3f8b61a0.try.emdashcms.com, https://3f8b61a0-emdash-playground.emdash-cms.workers.dev 0fcb9cf 2026-09-17T13:28:49.881Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://e07d9b67.try.emdashcms.com, https://e07d9b67-emdash-playground.emdash-cms.workers.dev 74a469a 2026-09-17T12:32:36.706Z Visit the dashboard ↗

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs d65daba Sep 19 2026, 05:20 PM

@ascorbic
ascorbic force-pushed the feat/sandbox-media-access branch from 74a469a to 3798860 Compare September 17, 2026 13:10
@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Sep 17, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR adds a well-scoped, capability-separated sandbox media-access slice. The three authorities (media:read, media:bytes:read, media:metadata:write) are kept properly isolated in the plugin context factory, both sandbox bridges, and the manifest/lexicon layers. Byte reads are bounded against the consumed stream rather than the stored size, metadata patches are constrained to alt/caption/focal points, and the new authenticated asset route hides storage keys as intended. Tests cover the bridges, runtime host, and auth boundary.

I found two concrete issues that should be fixed before merge:

  1. Authenticated asset route uses public cache headers. The new /_emdash/api/media/asset/[id]/[filename] route requires media:read, but it reuses the public cache constants designed for the unauthenticated, storage-key-based file route. public lets shared caches store authenticated responses, and immutable for non-image types is unsafe because the URL is ID-based (the underlying file can change while the URL stays the same). It should use private caching at most, or private, no-store.

  2. Cloudflare bridge mediaReadBytes does not align maxBytes validation with the workerd bridge. packages/cloudflare/src/sandbox/bridge.ts passes maxBytes straight to readPluginMediaBytes, which yields a different error message than the workerd bridge's explicit type guard. The bridge files explicitly promise "same error messages," so this is a parity gap.

Neither issue blocks the conceptual approach; both are localized fixes.

Comment thread packages/core/src/astro/routes/api/media/asset/[id]/[filename].ts Outdated
Comment thread packages/cloudflare/src/sandbox/bridge.ts
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Sep 17, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review cla: signed and removed review/awaiting-author Reviewed; waiting on the author to respond labels Sep 17, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The three sandbox media authorities are implemented consistently across core, the manifest/lexicon layers, the plugin CLI, admin consent labels, both sandbox bridges (Cloudflare Worker Loader and workerd), and the runtime-backed plugin test host. The authenticated asset route hides storage keys and rejects logged-out requests before touching the media table; byte reads bound the consumed stream instead of the stored size; and metadata patches are constrained to alt, caption, and complete focal-point pairs.

Both reviewed-head blockers from the previous review are fixed:

  • The opaque asset route now uses Cache-Control: private, max-age=0, must-revalidate, replacing the unsafe public/immutable cache headers.
  • The Cloudflare bridge now validates maxBytes with the same media/readBytes: maxBytes must be a number error as the workerd bridge.

I read the new plugin-media module, the route, the repository change, both bridges and their wrappers, the runtime wiring, the registry changes, the docs, and the changeset. I did not find any new logic bugs, regressions, or AGENTS.md convention violations. The implementation matches the documented contract, tests cover the capability separation and byte-limit paths, and the documentation claims align with the code.

This is a clean, well-scoped slice. LGTM.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@github-actions github-actions Bot added the review/approved Approved; no new commits since label Sep 17, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Review of 0fcb9cfab. Two blockers.

1. The new capabilities can never be published — the registry rejects them

/** Must stay in sync with PLUGIN_CAPABILITIES in packages/core/src/plugins/manifest-schema.ts */
const VALID_CAPABILITIES = [ ... "media:read", "media:write", ... ] as const;
...
	capabilities: z.array(z.enum(VALID_CAPABILITIES)),

packages/marketplace/src/routes/author.ts:254-279, consumed at :295 and :829

This PR adds media:bytes:read and media:metadata:write to every other enumeration site — packages/core/src/plugins/manifest-schema.ts:32-34, plugin-types, plugin-cli, admin consent labels, the lexicon, and the regenerated apps/release-action/dist/index.js — but packages/marketplace isn't touched by the diff at all. Its list still ends at media:read / media:write (:261-262).

Failure scenario: the marketplace Worker is the live distribution path (packages/core/src/plugins/marketplace.ts:225-242 fetches /api/v1/plugins… from it). An author who follows the new docs and declares media:bytes:read gets 400 {"error":"Invalid manifest: capabilities.0: Invalid enum value…"} on POST /plugins/:id/versions. The capability ships in core and appears in the consent UI, but no registry plugin can ever use it.

This is the established contract rather than a guess: the last capability-adding PR updated this exact list (7c5de08f6, taxonomies:read, #1719), and the sibling slice on feat/sandbox-redirects patches packages/marketplace/src/routes/author.ts too.

Fix: add "media:bytes:read" and "media:metadata:write" to VALID_CAPABILITIES after "media:read".

2. ctx.media.list() is unbounded below — a negative limit dumps the whole media table

const limit = Math.min(options.limit || 50, 100);

packages/core/src/database/repositories/media.ts:433, reached from packages/core/src/plugins/context.ts:550

On the Cloudflare path opts is plugin-supplied and passed straight through:

const db = new Kysely<Database>({ dialect: new D1Dialect({ database: this.env.DB }) });
return createMediaAccess(db).list(opts);

packages/cloudflare/src/sandbox/bridge.ts:921

With limit: -2, Math.min(-2, 100)-2, so Kysely emits limit -1, and SQLite/D1 treat a negative LIMIT as no limit. Verified locally:

sqlite> SELECT count(*) FROM (SELECT * FROM t LIMIT -1);   -- 5 of 5 rows

rows.slice(0, -2) then returns nearly every row and hasMore = rows.length > -2 is always true. A plugin holding only the low-risk-looking media:read calls ctx.media.list({ limit: -2 }) and gets the entire media library in one RPC response, with the unbounded D1 result buffered in the host isolate; repeated calls are a cheap host-side memory/CPU amplifier.

The workerd bridge is unaffected because it still clamps (bridge-handler.ts:1263, Math.max(1, Math.min(Number(opts.limit) || 50, 100))) — so after this PR the two bridges disagree on the bound.

To be straight about provenance: the same hole exists on main today, so this PR doesn't introduce it. But it rewrites exactly these functions to route the Cloudflare bridge through the unclamped helper, which is what makes the two bridges diverge.

Fix: clamp low as well as high in MediaRepository.findMany (and findPage): const limit = Math.max(1, Math.min(Math.trunc(Number(options.limit) || 50), 100));


Checked and clean: no plugin-supplied key reaches StoragereadPluginMediaBytes resolves item.storageKey from the DB after a status === "ready" check, and adapter errors are replaced with a key-free message. No new getSignedUploadUrl surface is exposed to sandboxes. Byte reads are bounded on the real stream (10 MiB default / 16 MiB max, thrown mid-stream with reader.cancel()). The metadata patch parser rejects every key outside alt|caption|focalX|focalY including __proto__-style own keys, and enforces the both-or-neither [0,1] focal contract. toPluginMediaItem omits storageKey, authorId and contentHash. The new asset route sits behind auth middleware (not in PUBLIC_API_PREFIXES) plus requirePerm(user, "media:read"), and keeps nosniff + CSP sandbox + attachment for non-safe types, so there's no new inline-HTML/SVG XSS path. Tests are runtime-backed (real LocalStorage, real workerd process, Worker Loader host).

~ 🤖 Codex

@ascorbic

Copy link
Copy Markdown
Collaborator Author

Fixed both blockers in 044b22f66.

  • Marketplace registration and bundle publication now accept media:bytes:read and media:metadata:write, with a regression test covering both validation boundaries.
  • MediaRepository.findMany() and findPage() now normalize limits to an integer in 1..100; negative/zero/fractional/non-number cases are covered. Workerd now delegates numeric normalization to the same shared boundary, and the Cloudflare D1 test asserts the bounded SQL limit.

Validated with root build, full typecheck/lint, marketplace tests (45), workerd bridge tests (52), Cloudflare bridge tests, dialect-backed repository tests, and a real-workerd media integration run. The final adversarial re-review found no bugs.

~ 🤖 Codex (gpt-5.6-sol)

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Sep 19, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Merge-order hazard with the sibling marketplace fix

#3170 and #3172 both change packages/marketplace/src/routes/author.ts, and they conflict. Confirmed with git merge-tree against their merge base a4af5781:

packages/marketplace/src/routes/author.ts
+<<<<<<< .our
+=======
+>>>>>>> .their

The two took different approaches to the same bug:

The conflict is loud, so nothing slips through silently at merge time. The risk is in the resolution: keeping #3172's hardcoded block would discard the shared import and re-break comments:read / comments:moderate publication, reintroducing the original bug.

Correct resolution: take #3170's shared PLUGIN_CAPABILITIES plus the @emdash-cms/plugin-types dependency, and drop #3172's marketplace edit. The shared list is a superset once both branches' capability additions land in plugin-types, so nothing is lost. Practically: merge #3170 first, then rebase #3172 to drop its author.ts change.

Worth noting this affects more than these two — #3185 (hooks.content-policy:register) and #3194 (content:publish/content:restore) have the same unpublishable-capability gap and neither touches author.ts. #3170's shared-list approach fixes all four at once, which is another argument for landing it first.

~ 🤖 Codex (claude-opus-5)

…cess

# Conflicts:
#	packages/cloudflare/src/sandbox/bridge.ts
@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Sep 19, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR is a well-scoped, architecture-fitting addition: it splits media authority into three separate capabilities (media:read, media:bytes:read, media:metadata:write) and threads them through the runtime, both sandbox bridges, CLI/lexicon validation, admin consent labels, and docs. The implementation generally follows EmDash conventions — no raw SQL, capability gating before storage access, bounded byte reads enforced on the consumed stream, and the authenticated asset route rejects anonymous requests before touching D1.

I read the core media helpers, the asset route, RBAC, the Cloudflare and workerd bridge changes, plugin-type round-tripping, the admin marketplace labels, the test host, and the changed docs/changesets. The test coverage is thorough across runtimes and boundaries.

Two real issues survive:

  1. updatePluginMediaMetadata() invalidates the site-settings cache, which has nothing to do with media metadata and will cause unnecessary cache churn.
  2. The public docs, skill, and changeset all say the authenticated asset route is for administrator sessions / API tokens with media:read, but the route only checks the RBAC media:read permission, whose minimum role is SUBSCRIBER. That claim is false and misleading about the access-control model.

Everything else is clean. I did not run tests, lint, or builds (static review only).

Comment thread packages/core/src/plugins/media.ts Outdated
Comment thread docs/src/content/docs/plugins/creating-plugins/capabilities.mdx Outdated
Comment thread skills/creating-plugins/SKILL.md Outdated
Comment thread .changeset/calm-media-sandboxes.md Outdated
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Sep 19, 2026
…cess

# Conflicts:
#	docs/src/content/docs/plugins/creating-plugins/api-routes.mdx
#	docs/src/content/docs/plugins/creating-plugins/capabilities.mdx
#	docs/src/content/docs/plugins/creating-plugins/testing.mdx
#	docs/src/content/docs/plugins/installing.mdx
#	packages/cloudflare/src/sandbox/bridge-runtime.ts
#	packages/cloudflare/src/sandbox/bridge.ts
#	packages/cloudflare/src/sandbox/runner.ts
#	packages/core/src/index.ts
#	packages/core/src/plugins/host.ts
#	packages/marketplace/src/routes/author.ts
#	packages/plugin-cli/src/init/templates.ts
#	packages/plugin-cli/tests/init-templates.test.ts
#	packages/plugin-cli/tests/manifest-trust-contract.test.ts
#	packages/plugin-test/src/runtime-host.ts
#	packages/plugin-test/test/fixture/src/plugin.ts
#	packages/plugin-test/test/host.test.ts
#	packages/plugin-types/src/index.ts
#	packages/plugin-types/tests/capabilities.test.ts
#	packages/workerd/src/sandbox/bridge-handler.ts
#	skills/creating-plugins/SKILL.md
#	skills/creating-plugins/references/sandbox-boundaries.md
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Sep 19, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This remains a well-scoped, architecture-fitting addition: it splits media authority into three separate capabilities (media:read, media:bytes:read, media:metadata:write) and threads them through the runtime, both sandbox bridges, CLI/lexicon validation, admin consent labels, and docs. The implementation follows EmDash conventions—capability gating before storage access, bounded byte reads enforced on the consumed stream, and the authenticated asset route rejecting anonymous requests before touching D1.

Prior review issues are both resolved.

  • updatePluginMediaMetadata() no longer invalidates the site-settings cache; that responsibility is correctly left with EmDashRuntime.handleMediaUpdate (where width/height can affect resolved site-setting URLs) and with createMediaAccessWithWrite().delete().
  • The public docs, skill, and changeset now accurately describe the authenticated asset route as requiring the RBAC media:read permission, whose minimum role is SUBSCRIBER. The new media-asset-auth.test.ts covers the logged-out 401 and Subscriber+ token authorization paths.

I re-read the core media helpers, the asset route and auth tests, the RBAC mapping, the Cloudflare and workerd bridge changes, the plugin-type round-tripping, the admin marketplace labels, the runtime test host, and the docs/changesets. Test coverage is thorough across runtimes and boundaries.

One small piece of documentation still lags the implementation: the PluginContext.media docstring in packages/core/src/plugins/types.ts still claims the field exists only for the legacy read:media/write:media capabilities, ignoring media:bytes:read and media:metadata:write and using deprecated capability names.

Everything else is clean. I did not run tests, lint, or builds (static review only).


Findings

  • [suggestion] packages/core/src/plugins/types.ts:877

    The PluginContext.media docstring still says the field exists "only if read:media or write:media capability". That is no longer true: PluginContextFactory.createContext populates media whenever any media capability is declared, including the new media:bytes:read and media:metadata:write. The capability names in the comment are also the deprecated legacy forms rather than the canonical media:read / media:write.

    	/** Media access - only if any media capability is declared */
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Sep 19, 2026
@ascorbic
ascorbic enabled auto-merge (squash) September 19, 2026 18:16
@ascorbic
ascorbic merged commit 2818e66 into main Sep 19, 2026
57 checks passed
@ascorbic
ascorbic deleted the feat/sandbox-media-access branch September 19, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant