feat(plugins): add sandboxed comment administration - #3170
Conversation
🦋 Changeset detectedLatest commit: 943f64b The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
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 |
Scope checkThis PR changes 1,993 lines across 59 files. Large PRs are harder to review and more likely to be closed without review. 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. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 943f64b | Sep 19 2026, 04:31 PM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-test
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-loader
@emdash-cms/registry-moderation
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
The approach is sound and well-scoped: it extends EmDash’s existing capability model to comments with clear separation between read (comments:read) and moderation (comments:moderate, which implies read), uses expected-status preconditions for safe concurrent moderation, and carries the same contract through native plugin contexts, the Cloudflare sandbox bridge, the workerd bridge, manifest/lexicon declarations, and admin consent labels. It fits the existing architecture without touching logged-out hot paths.
I traced the core path end-to-end: CommentRepository.updateStatusIf correctly atomically guards on the expected status; EmDashRuntime fences recursive and overlapping moderation through both an AsyncLocalStorage guard and an in-progress set; the admin route delegates through handleCommentModerate with the observed previous status so admins keep the same restore-from-trash behavior; and both the Cloudflare PluginBridge and workerd bridge-handler validate plugin-provided statuses before touching the runtime. The plugin-test host, bridge tests, and repository tests cover status conflicts, recursion blocking, personal-data shape, and runtime bridge round-trips.
I found only two small issues: a stale comment that no longer matches the awaited behavior, and a changeset paragraph that describes an internal test helper rather than user-facing release impact.
|
Review of Blocker: the new capabilities can never be published — the marketplace rejects them/** Must stay in sync with PLUGIN_CAPABILITIES in packages/core/src/plugins/manifest-schema.ts */
const VALID_CAPABILITIES = [ ... "content:write", "taxonomies:read", ... ] as const;
Failure scenario: an author publishes a bundle whose It fails closed, so it isn't a privilege issue — but the feature is broken end-to-end on that path. It's also the invariant the file's own comment states, and the precedent capability-addition PR (#1719, Fix: add Worth noting this is the same class of gap as #3172, which needs Checked and clean: ~ 🤖 Codex |
|
Addressed the marketplace publication blocker in 779d3f7. The marketplace now imports the canonical PLUGIN_CAPABILITIES tuple from @emdash-cms/plugin-types instead of maintaining a local copy, so comments:read and comments:moderate are accepted consistently for plugin registration, uploaded version manifests, and the /dev routes that reuse manifestSchema. I added a regression test covering both capabilities at both schema boundaries. Validated with the full marketplace suite (47/47), marketplace typecheck, plugin-types tests (71/71), lint, lockfile verification, and a Wrangler dry-run. An independent adversarial review of the exact new head found no blockers. |
Merge-order hazard with the sibling marketplace fix#3170 and #3172 both change 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 Correct resolution: take #3170's shared Worth noting this affects more than these two — #3185 ( ~ 🤖 Codex (claude-opus-5) |
…tration # Conflicts: # packages/cloudflare/src/sandbox/bridge.ts
…tration # Conflicts: # pnpm-lock.yaml
…tration # Conflicts: # apps/release-action/dist/index.js # docs/src/content/docs/plugins/creating-plugins/capabilities.mdx # packages/admin/src/lib/api/marketplace.ts # packages/admin/tests/lib/marketplace.test.ts # packages/cloudflare/src/sandbox/bridge.ts # packages/cloudflare/src/sandbox/runner.ts # packages/cloudflare/src/sandbox/types.ts # packages/cloudflare/src/sandbox/wrapper.ts # packages/cloudflare/tests/sandbox/wrapper-context.test.ts # packages/core/src/emdash-runtime.ts # packages/core/src/index.ts # packages/core/src/plugins/context.ts # packages/core/src/plugins/define-plugin.ts # packages/core/src/plugins/index.ts # packages/core/src/plugins/manifest-schema.ts # packages/core/src/plugins/sandbox/index.ts # packages/core/src/plugins/sandbox/types.ts # packages/core/tests/integration/plugins/capabilities.test.ts # packages/core/tests/unit/plugins/define-plugin.test.ts # packages/marketplace/src/routes/author.ts # packages/plugin-cli/src/init/templates.ts # packages/plugin-cli/src/manifest/schema.ts # packages/plugin-cli/tests/manifest-trust-contract.test.ts # packages/plugin-test/src/runtime-host.ts # packages/plugin-test/test/fixture/emdash-plugin.jsonc # packages/plugin-test/test/fixture/src/plugin.ts # packages/plugin-types/src/declared-access.ts # packages/plugin-types/src/index.ts # packages/plugin-types/src/manifest-schema.ts # packages/plugin-types/tests/capabilities.test.ts # packages/plugin-types/tests/declared-access.test.ts # packages/plugin-types/tests/manifest-schema.test.ts # packages/registry-lexicons/lexicons/com/emdashcms/experimental/package/releaseExtension.json # packages/registry-lexicons/tests/types.test.ts # packages/workerd/src/sandbox/bridge-handler.ts # packages/workerd/src/sandbox/dev-runner.ts # packages/workerd/src/sandbox/runner.ts # packages/workerd/src/sandbox/wrapper.ts # packages/workerd/test/bridge-handler.test.ts # packages/workerd/test/wrapper-context.test.ts # skills/creating-plugins/SKILL.md
There was a problem hiding this comment.
Re-review: still the right change, and the prior blockers are resolved
This PR adds the sandboxed comment-administration capability slice (comments:read and comments:moderate) to the plugin runtime contract, the admin consent layer, the core moderation service, and both sandbox bridges (Cloudflare Worker Loader and workerd). The approach remains correct and well-scoped: it gives plugins explicit, consent-gated access to comment personal data and expected-status moderation, routes plugin transitions through the same core service as admin transitions so notifications and hooks behave identically, and avoids touching logged-out routes.
This is a re-review. My previous review approved the change after two findings were addressed, and I verified that those fixes are present:
- The
comment:afterModeratehook is now documented as awaited once per successful transition, not fire-and-forget. - The changeset now describes the user-facing release impact (new capabilities, exposed data, error codes, and behavior) rather than internal test helpers.
For this pass I re-read the full diff and traced the implementation end-to-end:
- Core moderation path:
CommentRepository.updateStatusIfatomically guards onexpectedStatusand returns clearupdated/unchanged/conflict/not_foundstates.moderateCommentfences same-comment recursion withAsyncLocalStorage, whileEmDashRuntime.moderateCommentWithOriginuses a per-runtime set to reject overlapping in-process transitions withCOMMENT_MODERATION_IN_PROGRESS. - Admin route:
packages/core/src/astro/routes/api/admin/comments/[id]/status.tsdelegates throughhandleCommentModerate, passes the observed prior status, checks the existingcomments:moderateRBAC permission, and mapsCommentStatusConflictErrorandCOMMENT_MODERATION_IN_PROGRESSto 409 responses. - Plugin access layer:
PluginContextFactoryonly exposesctx.commentswhen the plugin declarescomments:readorcomments:moderate;comments:moderateimpliescomments:readconsistently throughdefinePlugin,adaptSandboxEntry, the Cloudflare wrapper/runner, and the workerd bridge-handler.PluginCommentexcludesauthorUserIdas documented. - Bridge security: Capability gating happens on every bridge method; invalid plugin statuses are rejected before reaching the runtime callback; errors are translated into the documented
COMMENT_STATUS_*shapes. - SQL/data integrity: Comment queries are Kysely builder calls with parameterized values; no interpolation. Comment table queries do not filter on
localebecause comments are not localized content rows. - Changeset/docs: The changeset lists the affected published packages and describes the released capability and error contract. Public docs match the
PluginCommentshape and thecomments:moderateimplication rules. - Tests: The added tests cover capability gating, atomic concurrency, recursive moderation, runtime-bridge integration, admin HTTP integration, declared-access round-trips, and generated-wrapper behavior.
I found no new logic bugs, security gaps, missing authorization checks, or AGENTS.md convention violations. LGTM.
What does this PR do?
Adds the complete sandboxed comment-administration vertical slice with
comments:readandcomments:moderate. Moderation implies read access.ctx.commentscan get, count, and cursor-page through non-trashed comments. The read capability explicitly exposes author names and email addresses, comment bodies, pseudonymous IP hashes, user agents, and moderation metadata, but not linked EmDash user-account IDs. Installation and updates surface that personal-data access for consent.Authorized plugins can move comments between
approved,pending, andspamthrough the core moderation service. Each mutation requires the previously observed status, rejects stale writes withCOMMENT_STATUS_CONFLICT, identifies plugin origin, runscomment:afterModerateonce, preserves approval notifications, and fences recursive moderation across native and sandbox execution. Hard deletion and bulk replacement remain out of scope.The contract is carried through shared and core manifest validation, declared-access canonicalization, registry lexicons and generated types, the plugin CLI and generated authoring skill, native context, Cloudflare Worker Loader, Node/workerd, and runtime-backed plugin-test helpers. Public documentation and package changesets describe the same authority and result shape.
This is an independent capability from the maintainer-directed sandbox capability plan, built on the runtime-test foundation merged in #3162.
Type of change
This is maintainer-directed capability work; there is no separate Discussion URL.
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (targeted tests for this change)pnpm formathas been runmessages.pofiles included)AI-generated code disclosure
Screenshots / test output
Not applicable; this PR does not change rendered UI.
Validated locally:
git diff --check;A separate adversarial-review agent reviewed the exact
main...6ebf5b4988dea0a399434b7bb0690713e54dd1f4diff. Its three findings were fixed and the exact-SHA re-check passed with no remaining blockers.