Expose authenticated user to plugin route handlers via RouteContext
#1757
khoinguyenpham04
announced in
Roadmap
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
This Roadmap discussion mirrors #812: Expose authenticated
userto plugin route handlers viaRouteContext.Use this discussion to upvote the roadmap item and discuss priority, use cases, and product feedback. Keep implementation tracking, reproduction details, and PR-specific feedback on the source issue.
enhancement,area/plugins,roadmap/1.0,roadmap/plugins-registryOriginal Issue
Expose authenticated
userto plugin route handlers viaRouteContextSummary
The catch-all plugin API route resolves the authenticated user, uses it to enforce permissions, and then drops it before invoking the plugin's handler. Plugin route handlers therefore have no first-class way to know which user is making the request, even on private routes that EmDash has already authenticated. Please add the resolved
usertoRouteContextso plugin authors can write per-user logic safely. It is referenced in #721Current behavior
packages/core/src/astro/routes/api/plugins/[pluginId]/[...path].tsDownstream,
PluginRouteHandler.invoke()(packages/core/src/plugins/routes.ts:64) builds the route context frombaseContext + input + request + requestMeta.baseContextcomes fromPluginContextFactory.createContext(plugin)and contains plugin/storage/kv/log/site/url and the optionalusers?capability — none of which represent the caller.RouteContextitself (packages/core/src/plugins/types.ts:1068) confirms the omission:The host has the user. The plugin needs the user. The wire between them is cut.
Why this matters
A plugin that wants to do anything user-scoped on a private route currently has three bad options:
Concrete use case driving this ticket — Wondo's Stripe Connect plugin (
wondo-stripe):user:{userId}:stripein plugin KV; story payouts route viastory.author).userIdfrom the request body, because any authenticated session would then be able to start, complete, or mutate onboarding for another user.This pattern (plugin admin routes that act on behalf of the calling author/editor) is general — payments, integrations, per-user API keys, OAuth connections, plugin-managed preferences — every one of them needs the caller. The current shape forces this logic out of plugins and into bespoke first-party routes, which is the opposite of what a plugin system is for.
Proposed change
Make the resolved caller available on
RouteContext. Purely additive.packages/core/src/plugins/types.ts— add an optional field toRouteContext:packages/core/src/plugins/routes.ts— extendInvokeRouteOptionsand passuserinto the context:packages/core/src/astro/routes/api/plugins/[pluginId]/[...path].ts— forwarduser(already in scope) tohandlePluginApiRoute. The handler signature gains auserparameter; it's already gated to private routes by the existingrequirePerm/requireScopechecks earlier in the same function, so the value passed here is the validated caller.PluginRouteRegistry.invokeandEmDashHandlers.handlePluginApiRoute— thread theuserthrough.Semantics
ctx.useris the authenticated caller. Always present, because the route would have already returned 401/403 otherwise.public: true):ctx.userisundefined. Public routes intentionally skip auth, so no caller is bound.ctx.useris whateverlocals.userresolves to under token auth. If a token is not bound to a user (machine token),ctx.userisundefinedandctx.requestMeta/locals.tokenScopesremain the only signals — document this.Backward compatibility
Additive only. Plugins that don't read
ctx.userare unaffected. No breaking change to existing route handlers, no migration, no flag.Type considerations
Userlives in@emdash-cms/auth.@emdash-cms/corealready depends on it transitively (the catch-all route importsrequirePermfrom#api/authorize.js, which usesUser). Importing the type directly intoplugins/types.tsshould be fine; if it introduces a cycle, expose a minimalRouteCallerUsershape ({ id, email, role }plus what plugins reasonably need) and keep the fullUserserver-internal.Out of scope (intentionally)
content:beforeUpdate). Those have a different "who triggered this" model and deserve a separate discussion. This ticket is scoped toRouteContext.users?capability or RBAC. Caller identity is not the same as theread:userscapability and should not be gated by it. (Related but distinct: Pluggable authorization layer — user RBAC is too coarse for multi-tenant/delegation scenarios #52.)PluginContextitself. Routes are the right surface — hooks can fire without an HTTP request at all (cron, system events) and don't have a caller in the same sense.Acceptance
RouteContext.useris typed and documented.undefined.ctx.user.id; public route returnsnull.Reference
packages/core/src/astro/routes/api/plugins/[pluginId]/[...path].ts(L25 hasuser, L68 drops it)https://github.com/emdash-cms/emdash/blob/46f065a8e8d531cc4be020fa9998305c7fcb62a8/packages/core/src/astro/routes/api/plugins/[pluginId]/[...path].ts#L68
Beta Was this translation helpful? Give feedback.
All reactions