Debugging the EmDash Webhook Notifier Plugin in Sandbox Mode

How I tracked down two bugs preventing the webhook notifier plugin from working in Cloudflare's sandboxed V8 isolates — a TypeScript packaging issue and an unsupported code block language.

I wanted to set up webhook notifications for my EmDash blog — get pinged whenever content changes so I can trigger builds, sync with other services, or just keep a log. EmDash ships a webhook notifier plugin that can run in sandbox mode (isolated V8 workers on Cloudflare), which is the safer option for third-party code. But when I tried to open the plugin settings page, things went sideways.

The First Error: Unexpected token '{'

After enabling the webhook notifier in sandbox mode:

emdash({
  plugins: [formsPlugin()],
  sandboxed: [webhookNotifierPlugin()],
  sandboxRunner: sandbox(),
})

Navigating to the plugin settings page in the admin UI returned:

Plugin responded with 400: {"error":{"code":"ROUTE_ERROR","message":"Unexpected token '{'"}}

That Unexpected token '{' looked like a JavaScript parser error, not a runtime error. Which was weird — why would already-bundled code fail to parse?

Tracing the Sandbox Pipeline

I dug into how EmDash's sandbox infrastructure works on Cloudflare:

  1. The integration reads the plugin's entrypoint file with readFileSync() at build time
  2. The code gets embedded as a string in a virtual module
  3. At runtime, it's passed to Cloudflare's Worker Loader as a JavaScript module
  4. Worker Loader spins up a V8 isolate and executes the code

The critical detail: step 1 reads whatever file the package's exports map points to. I checked what @emdash-cms/plugin-webhook-notifier/sandbox resolves to:

./src/sandbox-entry.ts

A TypeScript file. Raw .ts, not compiled JavaScript.

The Worker Loader V8 isolate can only execute JavaScript. When it hit the first TypeScript type annotation — something like routeCtx: { input: unknown; request: { url: string } } — it choked on the { because that's not valid JS syntax.

The Fix

The plugin package needs to ship compiled JavaScript for its sandbox entrypoint. Since I couldn't wait for an upstream fix, I compiled it locally with esbuild:

npx esbuild node_modules/@emdash-cms/plugin-webhook-notifier/src/sandbox-entry.ts \
  --bundle --format=esm --platform=neutral \
  --outfile=node_modules/@emdash-cms/plugin-webhook-notifier/dist/sandbox-entry.js \
  --alias:emdash=./emdash-shim.mjs

The --alias flag is needed because definePlugin from emdash is just an identity function for standard-format plugins, and bundling all of emdash would pull in Node.js dependencies, Astro internals, and everything else. The shim is one line:

export function definePlugin(def) { return def; }

Then I updated the package's exports in package.json:

"./sandbox": "./dist/sandbox-entry.js"

Rebuilt, deployed — and the first error was gone.

The Second Error: Cannot read properties of undefined (reading 'classes')

With the sandbox now loading the plugin correctly, a new error appeared:

Cannot read properties of undefined (reading 'classes')

This one was in the admin UI's Block Kit renderer. The webhook plugin's settings page returns Block Kit blocks, including a code block for the payload preview:

{ type: "code", code: payloadPreview, language: "json" }

The Block Kit renderer uses Cloudflare's kumo component library. The CodeBlock component has a variant system where each supported language maps to a config object with a classes property:

KUMO_CODE_VARIANTS = {
  lang: {
    ts:   { classes: "", description: "TypeScript" },
    tsx:  { classes: "", description: "TypeScript JSX" },
    jsonc: { classes: "", description: "JSON with comments" },
    bash: { classes: "", description: "Shell/Bash" },
    css:  { classes: "", description: "CSS" },
  }
}

Notice what's missing? json. The plugin passes language: "json", the renderer does KUMO_CODE_VARIANTS.lang["json"].classes, gets undefined.classes, and crashes.

The fix was simple — change "json" to "jsonc" in the plugin source, rebuild the sandbox bundle, and redeploy.

What I Learned

TypeScript packages need compiled output for sandbox targets. Vite handles .ts imports fine during normal builds, but when code is read as a raw string and passed to a V8 isolate, there's no transpilation step. If your plugin targets sandbox mode, ship .js files.

Block Kit language values must match the component library's variant map. This is the kind of bug that's invisible until runtime — there's no validation when building the blocks server-side. A simple enum check would catch it early.

Sandbox errors surface as generic 400s. The error wrapping in handleSandboxedRoute catches everything and returns ROUTE_ERROR with the raw message. Knowing the pipeline — virtual module embedding, Worker Loader, RPC boundary, wrapper code, bridge — helps narrow down where in the chain things break.

Both fixes are currently in node_modules and will be lost on reinstall. The proper fix belongs upstream in the plugin package.

X:00 Y:00