fix(core): prevent stale on-page saves and publication - #3840
khoinguyenpham04 wants to merge 4 commits into
Conversation
🦋 Changeset detectedLatest commit: 8158d10 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 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,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. |
🚀 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 deploymentPreview Deployments by commit
|
There was a problem hiding this comment.
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:2243The PT verification treats any row
_revchange as aCONFLICT, 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:1135handleContentGetnow returns_revfor every caller withcontent:read. The token is only needed by editors/preview consumers, and it encodes the row version andupdated_at. Consider returning_revonly 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) }) }, };
@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: |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
721314d to
8158d10
Compare
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 8158d10 | Oct 04 2026, 08:05 PM |
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.
The server's existing lock policy, visual-action token, preview collection loading and multi-entry publication behavior are unchanged.
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.Discussion is not applicable: this fixes verified lost-update/publication behavior and preserves additive compatibility. No translation catalogs are included.
AI-generated code disclosure
Screenshots / test output
lang=aranddir=rtl.pnpm build,pnpm typecheck,pnpm typecheck:demos,pnpm lint,pnpm lint:quick,pnpm format, and zerolint:jsondiagnostics pass.