Skip to content

Nothing records who changed content: admin revisions have a null author, the MCP attributes by reassigning entry ownership, and hooks get no actor #2881

Description

@eisenbruch

Description

Nothing in EmDash records who made a content change. Every change is kept (revisions, plus the audit-log plugin), but the actor is not.

Three things combine to produce this, measured on a production site (0.36.0, Cloudflare Workers + D1, 15,000 revisions across 2,400 articles):

1. Revisions saved from the admin have author_id = NULL. revisions.author_id exists, and RevisionRepository.create() writes input.authorId ?? null. But PUT /_emdash/api/content/{collection}/{id} only passes an authorId that the client put in the body (_collection_/_id_.mjs, the updateBody branch), and the admin editor never sends one. The route has locals.user in hand and does not use it. Result on our site: 14,881 revisions with a null author, the newest written minutes ago from the admin editor.

2. The MCP does attribute, but by reassigning ownership. The MCP content tools send authorId: userId on every create and update. That does populate revisions.author_id (1,138 rows on our site, all from the MCP), but authorId on a content update is also the entry's owner column: handleContentUpdate writes it to ec_{collection}.author_id. So every entry the MCP touches has its owner silently changed to whoever holds the token. On our site, 432 articles are now "owned" by the one account that ran a bulk MCP pass, which is what content:edit_own / content:publish_own are evaluated against. The same field is being used for "who wrote this entry" and "who made this revision", and those are different questions.

3. Plugin hooks receive no actor. content:beforeSave / content:afterSave are invoked with { content, collection, isNew }. An audit-log plugin can record what changed and when, but not by whom, and there is no other way for it to find out.

The upshot for a multi-editor site: complete history, zero attribution. Two editors and an integration are writing to the same 2,400 entries and nobody can answer "who changed this?".

Expected

  • The REST content routes (and the inline-editing save path) set the revision author from the session user when the client does not supply one, the same way the MCP already does.
  • Revision attribution is stored separately from entry ownership, so attributing a revision never reassigns the entry. Concretely: revisions.author_id should be "who saved this revision", and ec_*.author_id should only change when someone explicitly changes the owner.
  • Hook payloads carry the acting user (id and role at least), so plugins can log it.

Steps to reproduce

  1. Sign in to the admin as user A; open a published entry in a collection with revisions; edit the body; wait for autosave.
  2. SELECT author_id FROM revisions ORDER BY id DESC LIMIT 1NULL.
  3. Connect the MCP with a token belonging to user B; content_update the same entry with any small change.
  4. The new revision has author_id = B (good), and ec_<collection>.author_id is now B as well: the entry's owner has changed without anyone asking for it.
  5. Add a plugin with a content:afterSave hook and log its argument: no user field.

Environment

  • emdash version: 0.36.0
  • Astro version: 7.2.10
  • Runtime: Cloudflare Workers with D1
  • Deployment: production
  • OS: macOS

Logs / error output

-- revisions grouped by whether an author was recorded, production site, 2026-09-03
author null: 14,881 rows   first 2026-08-28 00:17   last 2026-09-03 01:51   (admin editor)
author set:   1,138 rows   first 2026-09-02 00:56   last 2026-09-03 02:33   (MCP only)

-- ec_articles.author_id after one bulk MCP pass by a single user
NULL: 1,575    <that user>: 432    two other users: 60, 1

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions