Skip to content

fix(core): let sandboxed beforeSave hooks reject saves - #2858

Merged
ascorbic merged 3 commits into
mainfrom
fix/sandboxed-before-save-rejection
Sep 2, 2026
Merged

ascorbic merged 3 commits into
mainfrom
fix/sandboxed-before-save-rejection

Conversation

@ascorbic

@ascorbic ascorbic commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Lets sandboxed content:beforeSave hooks reject content creation and updates with a bounded editor-facing reason. A sandboxed hook returns a typed version 1 SAVE_REJECTED result; the host validates the envelope, trims the plain-text reason, enforces the exported 500-character limit, and returns the normal SAVE_REJECTED / 422 API envelope with the rejecting plugin ID in structured details.

The admin wraps that validated data in a localized host-controlled message (Plugin {id} rejected the save: {reason}). React continues to render both values as text. Empty, overlong, malformed, and unknown error results, plus unexpected sandbox exceptions, stop the write with the generic CONTENT_HOOK_ERROR response and do not expose exception messages or stacks.

The existing #2617 trusted-hook behavior is unchanged. Publishing and pre-publish validation, media hooks, and content:afterSave remain out of scope.

Closes #2197

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 (the configured formatters were scoped to changed files to preserve unrelated untracked worktree files)
  • 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: https://github.com/emdash-cms/emdash/discussions/... (n/a: this completes the sandboxed half of bug content:beforeSave cancellation returns an unstructured 500 instead of a safe API error #2197)
  • I have included screenshots below if this PR changes the UI (n/a: no component or layout change; focused admin coverage verifies the localized text wrapper)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: GPT-5 Codex

Screenshots / test output

No visual component or layout change.

pnpm typecheck
  34 workspace projects passed

pnpm lint
  type-aware lint passed with warnings denied

packages/core
  sandbox hook envelope + real create/update route + trusted regression tests
  22 tests passed

@emdash-cms/cloudflare
  Worker Loader hook transport
  2 tests passed

@emdash-cms/sandbox-workerd
  real workerd HTTP transport
  8 tests passed

@emdash-cms/admin
  localized plain-text save rejection wrapper
  15 tests passed

Affected package builds passed for emdash, @emdash-cms/admin,
@emdash-cms/cloudflare, and @emdash-cms/sandbox-workerd.

pnpm --dir docs build
  passed

git diff --check
  passed

@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2cc7ab8

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

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/admin 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/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/release-service 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 github-actions Bot added the review/needs-review No maintainer or bot review yet label Sep 2, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 2, 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 fd703ed Sep 02 2026, 01:08 PM

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 702 lines across 26 files. Large PRs are harder to review and more likely to be closed without review.
This PR spans 5 different areas (area/core, area/admin, area/docs, area/templates, 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.

@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://fix-sandboxed-before-save-rejection.try.emdashcms.com, https://fix-sandboxed-before-save-rejection-emdash-playground.emdash-cms.workers.dev (commit 2cc7ab8)

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://3edb75eb.try.emdashcms.com, https://3edb75eb-emdash-playground.emdash-cms.workers.dev 2cc7ab8 2026-09-02T14:44:52.816Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://64869575.try.emdashcms.com, https://64869575-emdash-playground.emdash-cms.workers.dev fd703ed 2026-09-02T13:10:24.247Z Visit the dashboard ↗

@pkg-pr-new

pkg-pr-new Bot commented Sep 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

npm i https://pkg.pr.new/emdash@2858

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-moderation

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 2cc7ab8

@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 is a focused, well-scoped bug fix for the sandboxed half of #2197. The approach is sound: introduce a versioned __emdashSandboxHookResult envelope, validate it at the sandbox boundary, return the existing SAVE_REJECTED error shape with plugin id and reason in details, and let the admin wrap that in a localized message. I checked the new inspectSandboxHookResult validator, the runtime hook dispatch in emdash-runtime.ts, the admin client error formatting, the workerd/Cloudflare transport tests, and the updated docs/changeset.

Code-wise this looks solid: malformed/overlong/empty envelopes and sandbox exceptions all fall back to CONTENT_HOOK_ERROR without leaking internal strings, the save is stopped before persistence on rejections, and the existing trusted-hook path is untouched. The tests cover the validation boundary, real create/update routes, the workerd/Cloudflare RPC transport, and admin UI text wrapping.

The only block of issues is documentation drift: the prose was updated to say sandboxed hooks can return a rejection envelope, but the quick-reference tables and the reference Return Value bullets still describe content:beforeSave as returning only "modified content or void". Those need to stay consistent with the shipped behavior.


Findings

  • [needs fixing] docs/src/content/docs/reference/hooks.mdx:83-86

    The prose right above says a sandboxed content:beforeSave hook can reject the save by returning a version-1 error envelope, but the Return Value bullet list still only documents modified content and void. This contradicts the new behavior.

    #### Return Value
    
    - Return modified content object to apply changes
    - Return a sandbox hook error envelope to reject the save with a bounded plain-text reason
    - Return `void` to pass through unchanged
    
  • [needs fixing] docs/src/content/docs/plugins/creating-plugins/hooks.mdx:139

    The opening sentence says content:beforeSave returns only modified content or void, but the section later documents the new sandbox rejection envelope. Update the first line so the summary matches the rest of the page.

    Runs before content is saved. Return modified content, a sandbox hook error result, or `void` to leave it unchanged.
    
  • [needs fixing] docs/src/content/docs/plugins/creating-plugins/hooks.mdx:407

    The hook overview table still lists the content:beforeSave return type as "Modified content or void", omitting the new rejection envelope that the surrounding prose describes.

    | `content:beforeSave`     | Before content save           | Modified content, rejection envelope, or `void` | No        |
    
  • [needs fixing] skills/creating-plugins/references/hooks.md:469

    The quick-reference table says content:beforeSave returns only "Modified content or void", while the prose earlier in the same file already documents the SandboxHookErrorEnvelope. The same outdated table is duplicated in every template under templates/*/.agents/skills/creating-plugins/references/hooks.md; update them all in lockstep.

    | `content:beforeSave`      | Before save          | `content:write`                  | Modified content, `SandboxHookErrorEnvelope`, or `void`   |
    

@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 2, 2026
@ascorbic

ascorbic commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed all four documentation consistency findings in 7d124491b:

  • added the sandbox rejection envelope to the reference return-value list;
  • updated the authoring-guide summary and quick-reference table;
  • updated the creating-plugins skill quick-reference table;
  • regenerated all template skill copies.

Verified with pnpm typecheck, pnpm --dir docs build, pnpm lint:quick, and git diff --check.

~ 🤖 Codex

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Sep 2, 2026
@ascorbic
ascorbic enabled auto-merge (squash) September 2, 2026 14:35
@ascorbic
ascorbic merged commit bb8b087 into main Sep 2, 2026
50 checks passed
@ascorbic
ascorbic deleted the fix/sandboxed-before-save-rejection branch September 2, 2026 15:43
@emdashbot emdashbot Bot mentioned this pull request Sep 2, 2026
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.

content:beforeSave cancellation returns an unstructured 500 instead of a safe API error

1 participant