fix(core): enforce revision checks for content lifecycle mutations - #2783
Conversation
🦋 Changeset detectedLatest commit: 8a8e276 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 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 |
There was a problem hiding this comment.
The approach is sound: adding an optional _rev precondition to the publish/unpublish/discard-draft lifecycle mutations is the right fix for the concurrent-edit race described in #2782, and it follows the same optimistic-concurrency shape already used by handleContentUpdate. The implementation is consistent through the repository, handlers, runtime, Astro route wrappers, and ApiResult/ContentResponse types, and the new tests exercise the critical paths (happy path, stale _rev, concurrent publishes, malformed token, backward compatibility, unpublish/discard parity).
I didn't find any logic bugs, regressions, security issues, or AGENTS.md violations in the changed code: the conditional UPDATE is properly parameterized, _rev is optional so legacy callers keep working, ContentMutationConflictError is mapped to 409 CONFLICT, and the changeset is a reasonable patch entry.
The only notable gap is that the MCP content_publish, content_unpublish, and content_discard_draft tools were not updated to accept _rev, even though the lower-level handlers now support it and the content_update MCP tool already exposes _rev. That's a surface inconsistency for MCP clients and leaves them without the same race-condition guard. I left a line-anchored suggestion on the publish tool; the same _rev input and pass-through should be added to the unpublish and discard tools for parity.
Findings
-
[suggestion]
packages/core/src/mcp/server.ts:1260The REST route and runtime handler now accept an optional
_revprecondition for publish, but the MCPcontent_publishtool input schema does not. The siblingcontent_updatetool already exposes_rev, so these lifecycle tools should match so MCP clients can guard against concurrent edits too. Consider adding_revto the input schema and passing it through tohandleContentPublish. The same applies tocontent_unpublishandcontent_discard_draft.inputSchema: z.object({ collection: z.string().describe("Collection slug"), id: z.string().describe("Content item ID or slug"), publishedAt: z.iso .datetime({ offset: true, message: "must be an ISO 8601 datetime" }) .optional() .describe( "Override publication timestamp (ISO 8601). Requires content:publish_any permission. Useful when importing content with original publish dates.", ), _rev: z .string() .optional() .describe("Revision token from content_get for conflict detection"), }),
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. |
|
Addressed the EmDashBot MCP parity finding in |
@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-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@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: |
What does this PR do?
Adds an optional
_revprecondition to publish, unpublish, and discard-draft mutations across the REST and MCP APIs. The repository includes the expected version and timestamp in the final conditional update, so a writer save or autosave after approval causes aCONFLICTresponse instead of publishing unapproved content.Successful lifecycle mutations return the item and its next
_rev. Existing calls that omit_revkeep their current behavior.Closes #2782
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runThe changed files pass type-aware lint and
pnpm lint:quick. The repository-widepnpm lint:jsoncurrently reports 115 diagnostics, all outside this change.AI-generated code disclosure
Screenshots / test output
Non-visual change.
958b29a259d90c1edd37c5a65dbf93fbd68d1c0a