Add Mint plugin to Pascal - #591
Conversation
Aymericr
left a comment
There was a problem hiding this comment.
Thanks for this — the integration wiring is genuinely well done, and I want to be specific about that before the blockers, because most of what I'd normally push back on you already got right:
- Nothing loads or phones home until a user opts in.
defaultInstalled: falsepluscomponent: () => import('./panel')means the panel is lazy and the plugin registers metadata only. I checked for boot-time fetches and found none. - The API surface you depend on is tiny —
useEditor,useViewer, and three types. That's about as small a coupling as a plugin this size can have. - The proxy is an allowlist, not a passthrough, and it holds:
api/admin → 404 not_found api/me → 401 session_expired (no upstream call without a session) - Tokens really do stay server-side. HttpOnly,
Path=/api/plugins/mint,SameSite=Lax, PKCE S256, andtimingSafeEqualon the state compare. The token never crosses into browser code, as your summary claims. - Gates are clean on a rebase onto current
main:check1601 files,check-types9/9,build7/7. I also booted the built app and the route answers{"connected":false}unauthenticated.
Three things block it.
1. The CSRF origin check is bypassable
sameOrigin() compares the Origin header against requestOrigin(request) — but requestOrigin() derives its value from x-forwarded-host / x-forwarded-proto, which are attacker-controlled on the request. So the check compares attacker input against attacker input.
Against the built app:
POST /api/plugins/mint/logout
Origin: http://evil.test
x-forwarded-host: evil.test
→ 200 {}
versus the same request without the forged header:
POST /api/plugins/mint/logout
Origin: http://evil.test
→ 403 origin_mismatch
Same for the mutating proxy route: models:generate goes from 403 origin_mismatch to 401 session_expired — meaning it passed the origin gate and got as far as auth. With a session cookie present it would have gone upstream.
SameSite=Lax blunts the impact (it suppresses the cookie on cross-site POST), so this is defence-in-depth failing rather than a wide-open hole — but it's the only origin check the route has, and it doesn't hold.
2. x-forwarded-host rewrites the OAuth redirect_uri
Same root cause, more direct:
GET /api/plugins/mint/oauth/start
x-forwarded-host: evil.test
→ 302 https://mcp.mint.gg/oauth/authorize
?redirect_uri=http%3A%2F%2Fevil.test%2Fapi%2Fplugins%2Fmint%2Foauth%2Fcallback
&state=…&code_challenge=…
An authorization code is one redirect away from a host the attacker chose. Whether it lands there depends entirely on Mint's server-side redirect_uri allowlist — which is the right defence, but it's a defence living in someone else's codebase, and this route hands it a value it shouldn't.
The Secure flag has the same dependency: secureCookies() reads the spoofed proto, so x-forwarded-proto: http produces token cookies with no Secure attribute on an HTTPS deployment.
set-cookie: mint_pascal_state=…; Path=/api/plugins/mint; HttpOnly; SameSite=Lax; Max-Age=600
(no Secure.)
The fix is in the plugin, not this PR: derive the origin from configuration rather than request headers — handleMintPascalRequest already takes an options object, so a required origin (or an env var like the existing MINT_PASCAL_* ones) fits the existing shape. Trusting x-forwarded-* is only safe behind a proxy that overwrites them, and a self-hosted docker compose up — which this repo now ships — is not that.
3. The dependency is pinned to a mutable tag
"@mint/pascal-plugin": "github:mintdotgg/mint-pascal-plugin#v0.1.4"v0.1.4 is a lightweight ref (bd3248d) that your side can force-push at any time. The lockfile pins the SHA today, but any lockfile refresh re-resolves the tag, and bun.lock conflicts get regenerated rather than hand-merged.
The existing precedent in this file is a full SHA, for exactly this reason:
"@pascal-app/plugin-trees": "github:pascalorg/plugin-trees#56d978cd9b409b716207b3f3d269455d3cd6f067"Please match it. This isn't about trust — plugin-trees is our own repo and it's pinned by SHA too. It's that 4058 lines of code entering the default bootstrap should be reachable only at a revision we reviewed.
Also: the compatibility claim is already stale
The body says "verified Pascal 1.0.0-beta.3 compatibility", and the peer range agrees:
"@pascal-app/core": ">=0.9.2 <1.0.0 || 1.0.0-beta.1 || 1.0.0-beta.3"
main is on 1.0.0-beta.4. Because these resolve as workspace links, bun installs without complaint and everything typechecks — so this is stale metadata rather than a functional break, and given how narrow your import surface is I doubt anything is actually wrong. But as written, the plugin declares itself incompatible with the only version of Pascal it will be running against. Enumerating exact prereleases will keep breaking on every bump; a range like >=1.0.0-beta.1 would not.
Not blocking, just noting: mintPlugin.nodes is empty and the panel arms assets for placement through armMintAssetForPlacement, so nothing here adds node kinds — good, that keeps it out of the schema surface.
Happy to land this once (1) and (2) are fixed upstream and the dep is SHA-pinned. If it helps, (1) and (2) are one change: thread a configured origin into requestOrigin() and stop reading x-forwarded-*. I'd start there, cut a v0.1.5, and update this PR to its SHA.
- register the Mint panel and plugin with Pascal Editor - add the same-origin OAuth and API proxy route - include Mint plugin styles and pin the public v0.1.3 package
4781d5b to
c04b2c2
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c04b2c2. Configure here.
Aymericr
left a comment
There was a problem hiding this comment.
All three blockers fixed, and I re-ran the same probes against a build of fbfad86f rather than reading the diff — these were security findings, so they get verified the way they were found.
1. CSRF origin check — the forged header no longer changes the outcome:
POST /api/plugins/mint/logout
Origin: http://evil.test
x-forwarded-host: evil.test → 403 origin_mismatch (was 200)
POST /api/plugins/mint/api/models:generate
Origin: http://evil.test
x-forwarded-host: evil.test → 403 origin_mismatch (was 401 — i.e. it had
cleared the origin gate and reached auth)
2. OAuth redirect_uri — pinned to the configured origin, forged host ignored:
GET /api/plugins/mint/oauth/start
x-forwarded-host: evil.test
→ redirect_uri=http%3A%2F%2Flocalhost%3A3311%2F…%2Fcallback (was evil.test)
And the Secure-flag half, against an HTTPS-configured origin with x-forwarded-proto: http:
set-cookie: mint_pascal_state=…; HttpOnly; SameSite=Lax; Secure; Max-Age=600
Secure is present now; before, the spoofed proto stripped it.
3. Dep pin — full SHA in both package.json and bun.lock (902c546dbaece6b31455c0dc394afe6b9cd136fe), matching the plugin-trees precedent. You also fixed the non-blocking point: the peer range is >=1.0.0-beta.1 <1.0.0 instead of enumerated prereleases, so it stops going stale on every bump, and the compatibility matrix now covers beta.4.
configuredOrigin() is the right shape — it validates rather than just accepting, rejecting credentials, paths, and non-HTTP(S) schemes, so a typo'd env var fails loudly instead of producing a subtly wrong redirect_uri. Both of my probes are now regression tests in src/server/index.test.ts, which is what I'd want for a fix like this.
Misconfiguration path, since I went looking for it: with MINT_PASCAL_HOST_ORIGIN unset, BASE_URL falls back to https://editor.pascal.app in production. That fails closed — a self-hoster's own origin gets 403 origin_mismatch rather than being trusted, and the fallback is our domain, not anything an attacker can influence. Compose defaults it to http://localhost:3000, so the shipped path works out of the box. Correct tradeoff.
Re-verified unchanged from my first pass: proxy allowlist holds (api/admin → 404, api/me → 401, no upstream call without a session), defaultInstalled: false with a lazy component: () => import('./panel'), no document.cookie / localStorage / access_token anywhere outside server/, and mintPlugin.nodes still empty so nothing enters the schema surface.
Gates on a rebase onto current main: check clean, check-types 9/9, build 7/7. Booted the built app and the route answers {"connected":false} unauthenticated.
Thanks for turning this around quickly and for fixing it upstream in the plugin instead of patching around it in the host route — that was the right place for it. Merging.

Summary
Mint tokens remain in host-only HttpOnly cookies and are never exposed to the browser package.
Tested
bun checkbun check-typesbun --filter editor buildPlugin release: https://github.com/mintdotgg/mint-pascal-plugin/releases/tag/v0.1.5
Note
Medium Risk
Introduces server-side OAuth/session handling for a third-party integration; token handling depends on the plugin’s HttpOnly cookie design but still expands the auth attack surface.
Overview
Wires Mint into the editor the same way as the trees plugin:
@mint/pascal-pluginis added (GitHub-pinned),mintPluginis discovered andmintHostPanelis registered at bootstrap, and the package is included in Next transpilation and Tailwind@sourceso its UI styles compile.Adds a same-origin catch-all API route at
app/api/plugins/mint/[...path]that delegates GET/POST tohandleMintPascalRequestwith the appBASE_URL, so Mint OAuth and API traffic run through the host rather than the browser bundle.Reviewed by Cursor Bugbot for commit c04b2c2. Bugbot is set up for automated code reviews on this repo. Configure here.