diff --git a/Dockerfile b/Dockerfile index cd12db324..b36abc8fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,9 @@ ENV NEXT_PUBLIC_DEFAULT_LOCALE=$NEXT_PUBLIC_DEFAULT_LOCALE # `git rev-parse` inside the build can't find it - CI must pass it in. ARG GIT_COMMIT=unknown ENV GIT_COMMIT=$GIT_COMMIT -RUN npx next build --webpack +# The mock JMAP server (DEV_MOCK_JMAP) accepts any password and is for +# `npm run dev` only; images ship without it. +RUN rm -rf "app/api/dev-jmap" && npx next build --webpack FROM node:24-alpine AS runner diff --git a/app/api/dev-jmap/[...path]/route.ts b/app/api/dev-jmap/[...path]/route.ts index d0319c2b9..00b2c1572 100644 --- a/app/api/dev-jmap/[...path]/route.ts +++ b/app/api/dev-jmap/[...path]/route.ts @@ -2471,7 +2471,11 @@ function tooLargeFor(method: string, args: MethodArgs, callId: string): MethodRe // --------------------------------------------------------------------------- function isDevMockEnabled(): boolean { - return process.env.DEV_MOCK_JMAP === 'true'; + // NEXT_PUBLIC_DEV_MOCK_JMAP is inlined at build time from DEV_MOCK_JMAP + // (next.config.ts `env`), so a build made without the mock - every + // release image and tarball - cannot switch it on through the runtime + // environment: the mock accepts any password. + return process.env.NEXT_PUBLIC_DEV_MOCK_JMAP === 'true' && process.env.DEV_MOCK_JMAP === 'true'; } function getBaseUrl(request: NextRequest): string { diff --git a/lib/__tests__/dev-jmap-mock.test.ts b/lib/__tests__/dev-jmap-mock.test.ts index 9623fee76..e3fc2d19f 100644 --- a/lib/__tests__/dev-jmap-mock.test.ts +++ b/lib/__tests__/dev-jmap-mock.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect, beforeAll, vi } from 'vitest'; // Mock the environment variable beforeAll(() => { vi.stubEnv('DEV_MOCK_JMAP', 'true'); + // Inlined from DEV_MOCK_JMAP at build time in a real build. + vi.stubEnv('NEXT_PUBLIC_DEV_MOCK_JMAP', 'true'); }); // We test via dynamic import to get a fresh module with env set @@ -369,3 +371,21 @@ describe('dev-jmap mock server', () => { }); }); }); + +// The mock accepts any password; a build made without DEV_MOCK_JMAP (every +// release image) must not be able to switch it on at runtime. +describe('dev-jmap mock in a build made without it', () => { + it('stays off even when DEV_MOCK_JMAP is set at runtime', async () => { + vi.stubEnv('NEXT_PUBLIC_DEV_MOCK_JMAP', ''); + try { + const { GET } = await loadRoute(); + const req = makeRequest('http://localhost:3000/api/dev-jmap/.well-known/jmap', { + headers: { host: 'localhost:3000' }, + }); + const res = await GET(req as never, { params: Promise.resolve({ path: ['.well-known', 'jmap'] }) }); + expect(res.status).toBe(404); + } finally { + vi.stubEnv('NEXT_PUBLIC_DEV_MOCK_JMAP', 'true'); + } + }); +});