From 9873cc17100c3b23df6b9baec588bbc9a691ce24 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 5 Oct 2026 23:14:26 -0400 Subject: [PATCH] API: also serve routes under /api A reverse proxy can now forward /api/ to the API with or without stripping the prefix (NPMplus custom locations don't strip it). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8 --- apps/api/src/app.ts | 10 +++++++++- apps/api/test/auth.test.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 8572652..3cbd094 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -22,7 +22,15 @@ import { webhookRoutes } from './routes/webhooks'; /** Builds the API without listening, so tests can drive it with app.inject(). */ export async function buildApp({ logger = true, rateLimits = true }: { logger?: boolean; rateLimits?: boolean } = {}) { - const app = Fastify({ logger, trustProxy: true }); + const app = Fastify({ + logger, + trustProxy: true, + // Serve /api/... as well as /..., so a reverse proxy can forward /api/ with or without stripping it. + rewriteUrl: (req) => { + const url = req.url ?? '/'; + return url === '/api' || url.startsWith('/api/') || url.startsWith('/api?') ? url.slice(4) || '/' : url; + }, + }); await app.register(cors, { origin: [env.WEB_URL], diff --git a/apps/api/test/auth.test.ts b/apps/api/test/auth.test.ts index a9fa98d..b390886 100644 --- a/apps/api/test/auth.test.ts +++ b/apps/api/test/auth.test.ts @@ -26,6 +26,18 @@ describe('auth', () => { expect(viaCookie.json().id).toBe(user.id); }); + it('serves the same routes under /api for proxies that keep the prefix', async () => { + const res = await app().inject({ + method: 'POST', + url: '/api/auth/register', + payload: { username: 'bob', email: 'bob@example.com', password: 'password123' }, + }); + expect(res.statusCode).toBe(200); + const me = await app().inject({ url: '/api/me?x=1', headers: bearer(res.json().token) }); + expect(me.json().username).toBe('bob'); + expect((await app().inject({ url: '/apiary' })).statusCode).toBe(404); + }); + it('stores only a hash of the session token', async () => { const { token } = await createUser(app(), 'alice'); const rows = await sql`select id from sessions`;