Skip to content

fix(core): prevent stale on-page saves and publication - #3840

Closed
khoinguyenpham04 wants to merge 4 commits into
noah/content-save-historyfrom
noah/content-visual-revisions
Closed

khoinguyenpham04 wants to merge 4 commits into
noah/content-save-historyfrom
noah/content-visual-revisions

Conversation

@khoinguyenpham04

Copy link
Copy Markdown
Collaborator

What does this PR do?

Prevents stale on-page edits from silently overwriting or publishing newer content. Layer 3 of the content-safety stack, based on #3834; no linked issue.

  • Carry the rendered revision into text, image and Portable Text editing without another logged-out database query. Legacy snapshots verify their displayed value before editing.
  • Serialize each entry's writes, advance tokens after successful own saves, and fence publication behind successful saves. A same-field editor cannot overwrite another instance's newer document.
  • Preserve local edits after conflicts or edit-lock refusals and show localized recovery guidance. Clean legacy annotations can still publish unchanged drafts.
  • Preserve save-before-publish when an in-flight save finishes after Undo. Page-leave writes remain guarded and best-effort, not a durability guarantee.

The server's existing lock policy, visual-action token, preview collection loading and multi-entry publication behavior are unchanged.

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: https://github.com/emdash-cms/emdash/discussions/...
  • I have included screenshots below if this PR changes the UI

Discussion is not applicable: this fixes verified lost-update/publication behavior and preserves additive compatibility. No translation catalogs are included.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: GPT-6.1 Sol (max), with iterative GPT-5.6 (xhigh) adversarial review in a persistent Codex session.

Screenshots / test output

On-page editor retaining local text after a stale save, showing conflict recovery guidance and refusing publication

  • Real SQLite and PostgreSQL regression fixtures: 229 visual/component/loader-preview cases pass. Broader loader/query coverage: 252 cases pass; these sets overlap. Admin toolbar localization: 2 cases pass.
  • The stale save returns 409; local text remains visible; the stored newer title is unchanged; clicking Publish sends no publication request. Actual Arabic admin verification confirms lang=ar and dir=rtl.
  • Full root pnpm build, pnpm typecheck, pnpm typecheck:demos, pnpm lint, pnpm lint:quick, pnpm format, and zero lint:json diagnostics pass.
  • Three persistent second-opinion cycles; the latest is clean with no remaining findings. The original findings were independently reproduced before their fixes, including the gated PUT → Undo → Publish race on both databases.
  • Remote CI is not claimed green: the Codex CI connection requires sign-in. No merge is requested.

@changeset-bot

changeset-bot Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8158d10

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

This PR includes changesets to release 12 packages
Name Type
emdash Patch
@emdash-cms/admin Patch
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-test Patch
@emdash-cms/sandbox-workerd 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/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 commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,219 lines across 19 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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://noah-content-visual-revisions.try.emdashcms.com, https://noah-content-visual-revisions-emdash-playground.emdash-cms.workers.dev (commit 8158d10)

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://fbbdf55e.try.emdashcms.com, https://fbbdf55e-emdash-playground.emdash-cms.workers.dev 8158d10 2026-10-04T20:07:04.151Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://36e89b58.try.emdashcms.com, https://36e89b58-emdash-playground.emdash-cms.workers.dev 721314d 2026-10-03T22:45:13.911Z Visit the dashboard ↗

@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 the right change for the stated problem: it carries a rendered revision token into the on-page editors, serializes writes per entry, advances the token after successful saves, and fences Publish behind a clean write queue. The implementation is largely sound and the test matrix is comprehensive.

I focused on the new session logic in InlinePortableTextEditor, the toolbar script, the loader/query token plumbing, and the API side of _rev. Statically, the overall approach fits EmDash: no new logged-out queries, no new migrations, strings go through Lingui, and the PUT/Publish endpoints keep their existing authorization checks.

I don’t see blocking logic bugs or security regressions, but I found two places where the Portable Text path behaves more conservatively than the raw text/image path or where the API surface is broader than necessary. Both are suggestions rather than blockers.


Findings

  • [suggestion] packages/core/src/components/InlinePortableTextEditor.tsx:2243

    The PT verification treats any row _rev change as a CONFLICT, even when the stored Portable Text data is identical to what the editor is showing. The raw text/image paths refresh the token when the displayed value still matches the stored value; doing the same here would let editors keep working after an unrelated field changed, instead of forcing a page refresh.

    					if (session.rev && session.rev !== rev) {
    						if (savedDoc.eq(editor.schema.nodeFromJSON(portableTextToPM(stored)))) {
    							session.rev = rev;
    							session.errors.delete(fieldKey);
    							fieldRevisionRef.current = session.fieldRevisions.get(field);
    							verifiedRef.current = true;
    							editor.setEditable(true);
    							return true;
    						}
    						session.errors.set(fieldKey, "CONFLICT");
    						document.dispatchEvent(
    							new CustomEvent("emdash:save", { detail: { state: "error", code: "CONFLICT" } }),
    						);
    						return false;
    					}
    
  • [suggestion] packages/core/src/api/handlers/content.ts:1135

    handleContentGet now returns _rev for every caller with content:read. The token is only needed by editors/preview consumers, and it encodes the row version and updated_at. Consider returning _rev only when the caller is allowed to write the entry (or when explicitly requested by edit-mode callers), so the API contract does not broaden for read-only users.

    		const canWrite = /* caller has content:edit_own/any on this item */ true;
    		return {
    			success: true,
    			data: { item, ...(canWrite && { _rev: encodeRev(item) }) },
    		};
    

@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 Oct 3, 2026
@khoinguyenpham04
khoinguyenpham04 added this pull request to stack #3844 October 4, 2026 01:18
@pkg-pr-new

pkg-pr-new Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-test

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-loader

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

@emdash-cms/registry-moderation

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 8158d10

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@khoinguyenpham04
khoinguyenpham04 force-pushed the noah/content-visual-revisions branch from 721314d to 8158d10 Compare October 4, 2026 20:03
@khoinguyenpham04 khoinguyenpham04 mentioned this pull request Oct 4, 2026
8 of 19 tasks
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 4, 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 8158d10 Oct 04 2026, 08:05 PM

@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 Oct 4, 2026
@khoinguyenpham04
khoinguyenpham04 removed this pull request from stack #3844 October 4, 2026 20:12
@khoinguyenpham04
khoinguyenpham04 added this pull request to stack #3866 October 4, 2026 20:12
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