feat(plugins): expose authenticated caller to route handlers as ctx.user#1947
Open
swissky wants to merge 1 commit into
Open
feat(plugins): expose authenticated caller to route handlers as ctx.user#1947swissky wants to merge 1 commit into
ctx.user#1947swissky wants to merge 1 commit into
Conversation
The plugin API catch-all resolves and authorizes the caller, then drops it before dispatch, leaving plugins no safe way to know who is calling a private route. Thread the validated user through handlePluginApiRoute into RouteContext (native format) and routeCtx.user (standard format, in-process and worker sandboxes alike) as the read-only UserInfo shape. Public routes and machine tokens with no bound user receive undefined; the catch-all only forwards the caller after private-route auth, so an ambient admin session is never bound to a public route. Fixes emdash-cms#812
🦋 Changeset detectedLatest commit: 872b71e The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 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 |
@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/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: |
Contributor
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. |
This was referenced Jul 12, 2026
This was referenced Jul 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
The plugin API catch-all resolves the authenticated user, uses it to enforce
plugins:manage+ scope + CSRF — and then drops it before invoking the plugin's handler. Plugins had no first-class way to know who is calling a private route, forcing bad workarounds (reading a user id from the request body is trivially spoofable).This threads the validated caller through
handlePluginApiRouteinto the route context, exactly as proposed in #812:ctx.useronRouteContext.routeCtx.user— wired through the in-process adapter, the Cloudflare Worker Loader wrapper, and both workerd runners (SerializedRequest.useris structured-clone/JSON-safe).UserInfo({ id, email, name, role, createdAt }) — the same shapectx.usersreturns, with sensitive host-side fields (avatarUrl,data, …) stripped bytoRouteCallerInfo().Semantics (per the issue):
ctx.useris the authenticated caller, validated by the existingrequirePerm/requireScopechecks one frame up.ctx.userisundefined— the catch-all only forwards the caller after private-route auth, so an ambient admin session browsing the site is never bound to a public route.undefined.Purely additive — no change for plugins that don't read
ctx.user, no migration, no flag. Caller identity is intentionally not gated by theusers:readcapability (it's the caller's own identity, not a directory lookup).Tests: route handler exposes/omits
ctx.user(unit), catch-all forwards the caller only for private routes (unit, including the ambient-session case), sandbox adapter putsuseronrouteCtxand strips it frompluginCtx,toRouteCallerInfomapping (Date→ISO, field stripping). Docs updated in the API-routes guide (new "The authenticated caller" section + route context reference) and the native-plugin guide.Closes #812
Discussion: #1757
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. — n/a, no admin UI stringsAI-generated code disclosure
Screenshots / test output