From a8638619e5473050a810dac817207278eb049a3b Mon Sep 17 00:00:00 2001 From: lempkey Date: Mon, 6 Apr 2026 15:00:39 +0100 Subject: [PATCH 1/2] fix: use Express 5 wildcard syntax for better-auth handler route Express 5 (path-to-regexp v8+) dropped support for the *paramName wildcard syntax from Express 4. The route registered as '/api/auth/*authPath' silently fails to match any sub-path, causing every /api/auth/* request to return 404 instead of reaching the better-auth handler. Fixes: #2898 Change the route to '/api/auth/{*authPath}', the correct named catch-all syntax in Express 5. --- .../__tests__/express5-auth-wildcard.test.ts | 47 +++++++++++++++++++ server/src/app.ts | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 server/src/__tests__/express5-auth-wildcard.test.ts diff --git a/server/src/__tests__/express5-auth-wildcard.test.ts b/server/src/__tests__/express5-auth-wildcard.test.ts new file mode 100644 index 00000000..d3e8d618 --- /dev/null +++ b/server/src/__tests__/express5-auth-wildcard.test.ts @@ -0,0 +1,47 @@ +import express from "express"; +import request from "supertest"; +import { describe, expect, it, vi } from "vitest"; + +/** + * Regression test for https://github.com/paperclipai/paperclip/issues/2898 + * + * Express 5 (path-to-regexp v8+) dropped support for the `*paramName` + * wildcard syntax used in Express 4. Routes declared with the old syntax + * silently fail to match, causing every `/api/auth/*` request to fall + * through and return 404. + * + * The correct Express 5 syntax for a named catch-all is `{*paramName}`. + * These tests verify that the better-auth handler is invoked for both + * shallow and deep auth sub-paths. + */ +describe("Express 5 /api/auth wildcard route", () => { + function buildApp() { + const app = express(); + const handler = vi.fn((_req: express.Request, res: express.Response) => { + res.status(200).json({ ok: true }); + }); + app.all("/api/auth/{*authPath}", handler); + return { app, handler }; + } + + it("matches a shallow auth sub-path (sign-in/email)", async () => { + const { app } = buildApp(); + const res = await request(app).post("/api/auth/sign-in/email"); + expect(res.status).toBe(200); + }); + + it("matches a deep auth sub-path (callback/credentials/sign-in)", async () => { + const { app } = buildApp(); + const res = await request(app).get( + "/api/auth/callback/credentials/sign-in" + ); + expect(res.status).toBe(200); + }); + + it("invokes the handler for every matched sub-path", async () => { + const { app, handler } = buildApp(); + await request(app).post("/api/auth/sign-out"); + await request(app).get("/api/auth/session"); + expect(handler).toHaveBeenCalledTimes(2); + }); +}); diff --git a/server/src/app.ts b/server/src/app.ts index 686ecfde..eaec70d4 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -133,7 +133,7 @@ export async function createApp( }); }); if (opts.betterAuthHandler) { - app.all("/api/auth/*authPath", opts.betterAuthHandler); + app.all("/api/auth/{*authPath}", opts.betterAuthHandler); } app.use(llmRoutes(db)); From fc8e1d11533da8fe14d1aa2ad6ddfa8e5442b097 Mon Sep 17 00:00:00 2001 From: lempkey Date: Mon, 6 Apr 2026 16:28:42 +0100 Subject: [PATCH 2/2] test: add over-broad route guard test and address Greptile review --- server/src/__tests__/express5-auth-wildcard.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/__tests__/express5-auth-wildcard.test.ts b/server/src/__tests__/express5-auth-wildcard.test.ts index d3e8d618..2771afe1 100644 --- a/server/src/__tests__/express5-auth-wildcard.test.ts +++ b/server/src/__tests__/express5-auth-wildcard.test.ts @@ -38,6 +38,15 @@ describe("Express 5 /api/auth wildcard route", () => { expect(res.status).toBe(200); }); + it("does not match unrelated paths outside /api/auth", async () => { + // Confirm the route is not over-broad — requests to other API paths + // must fall through to 404 and not reach the better-auth handler. + const { app, handler } = buildApp(); + const res = await request(app).get("/api/other/endpoint"); + expect(res.status).toBe(404); + expect(handler).not.toHaveBeenCalled(); + }); + it("invokes the handler for every matched sub-path", async () => { const { app, handler } = buildApp(); await request(app).post("/api/auth/sign-out");