Skip to content

Trusted-mode (plugins: []) route adapter calls standard-format handlers with the old two-arg signature, breaking the public single-arg RouteContext API #2079

Description

@marcusbellamyshaw-cell

Description

adaptSandboxEntry (src/plugins/adapt-sandbox-entry.ts) is used to run a standard-format plugin (one exporting definePlugin({ hooks, routes }) as its default export) trusted/in-process via plugins: [somePlugin()]. When wrapping a plugin's route handler, it always invokes it with the old two-argument SandboxedPlugin calling convention:

// adapt-sandbox-entry.ts (~L219-225)
const routeCtx = { input: ctx.input, request: requestShape, requestMeta: ctx.requestMeta };
const { input: _, request: __, requestMeta: ___, ...pluginCtx } = ctx;
return handler(routeCtx, pluginCtx);

But the public, exported, type-checked route handler contract from emdash's own types is single-argument:

// from `emdash`'s exported types (index-BXOCyLj0.d.mts)
interface PluginRoute<TInput = unknown> {
  handler: (ctx: RouteContext<TInput>) => Promise<unknown>;
}

A plugin author who writes routes: { myRoute: { handler: async (ctx: RouteContext) => { ... ctx.storage.foo ... } } } — which is what the exported RouteContext/definePlugin types require and what tsc accepts — gets ctx = routeCtx (the adapter's first positional argument) at runtime when run trusted, since JS silently drops the second argument for a function declared with one parameter. routeCtx only has { input, request, requestMeta } — no storage, email, kv, cron, users, etc. — so any handler that touches those throws Cannot read properties of undefined (reading '<whatever>').

This means every standard-format plugin using the public single-arg RouteContext API is broken the moment it's run trusted (plugins: []) if any route handler touches anything beyond ctx.input/ctx.request/ctx.requestMeta.

Related to (but distinct from) #2078 — that one is about sandboxed (sandboxed: []) route auth metadata never being populated for config-declared plugins. Between the two, there is currently no way to run a standard-format plugin with public routes that touch ctx.storage/ctx.email/etc. correctly: sandboxed: [] 401s every non-admin route, and plugins: [] throws on any route handler using more than the request-shaped fields.

Steps to reproduce

  1. Define a standard-format plugin with a route that reads ctx.storage:
    export default definePlugin({
      id: "demo",
      version: "1.0.0",
      storage: { things: { indexes: [] } },
      routes: {
        ping: {
          public: true,
          handler: async (ctx: RouteContext) => {
            const items = await ctx.storage.things!.query({ limit: 1 });
            return { ok: true, count: items.items.length };
          },
        },
      },
    });
  2. Register it trusted: emdash({ plugins: [demoPlugin()] }).
  3. curl http://localhost:4321/_emdash/api/plugins/demo/ping
  4. Server log shows: TypeError: Cannot read properties of undefined (reading 'query') (or whichever field is accessed), thrown from inside the plugin's own handler at the line touching ctx.storage, because ctx at runtime is the adapter's routeCtx (missing everything but input/request/requestMeta), not the full PluginContext.

Found while trying to work around #2078 by running emdash-plugin-engagement trusted instead of sandboxed — its leaderboard route (ctx.storage.points!.query(...)) throws exactly this way.

Environment

  • emdash version: 0.29.0
  • Node.js version: 24.15.0 (local dev)
  • Runtime: Node (Astro dev server, @astrojs/cloudflare adapter)
  • OS: Windows 11

Logs / error output

[ERROR] [vite] [plugin:engagement] Route handler failed: TypeError: Cannot read properties of undefined (reading 'points')
    at handler (.../emdash-plugin-engagement/dist/sandbox-entry.js:301:44)
    at Object.handler (.../emdash/src/plugins/adapt-sandbox-entry.ts:225:13)
    at PluginRouteHandler.invoke (.../emdash/src/plugins/routes.ts:148:31)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions