diff --git a/lib/__tests__/next-config-cors.test.ts b/lib/__tests__/next-config-cors.test.ts new file mode 100644 index 000000000..3f986d29a --- /dev/null +++ b/lib/__tests__/next-config-cors.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { NextConfig } from 'next'; + +async function loadConfig(basePath?: string): Promise { + vi.resetModules(); + vi.stubEnv('NEXT_PUBLIC_BASE_PATH', basePath); + + const { default: config } = await import('../../next.config'); + return config; +} + +afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); +}); + +describe('Next static asset CORS', () => { + it.each([undefined, '/webmail'])('allows opaque plugin sandboxes with basePath %s', async (basePath) => { + const config = await loadConfig(basePath); + const rules = await config.headers?.(); + + expect(config.basePath).toBe(basePath); + expect(rules?.filter((rule) => rule.source.includes('_next/static'))).toEqual([ + { + source: '/_next/static/:path*', + headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }], + }, + ]); + }); + + it('does not add CORS headers to application or API routes', async () => { + const config = await loadConfig('/webmail'); + const rules = await config.headers?.(); + const corsRules = rules?.filter((rule) => + rule.headers.some((header) => header.key.toLowerCase() === 'access-control-allow-origin') + ); + + expect(corsRules?.map((rule) => rule.source)).toEqual(['/_next/static/:path*']); + }); +}); diff --git a/next.config.ts b/next.config.ts index 104b81868..57c062f57 100644 --- a/next.config.ts +++ b/next.config.ts @@ -46,6 +46,16 @@ const nextConfig: NextConfig = { // it from node_modules at runtime instead of trying to bundle it. Used by // PLUGIN_DEV_DIR's on-the-fly bundler. serverExternalPackages: ["esbuild"], + async headers() { + return [ + { + // Untrusted plugin iframes have opaque origins and load these public, + // immutable chunks in CORS mode. Next prefixes this source with basePath. + source: "/_next/static/:path*", + headers: [{ key: "Access-Control-Allow-Origin", value: "*" }], + }, + ]; + }, // Sibling repos checked out under ./repos/ are unrelated source trees that // Turbopack's NFT can otherwise rope into the trace when dynamic fs calls // confuse it. Keeps the build from ballooning memory tracing dead code.