Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions src/utils/corsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Selects the appropriate CORS middleware options based on environment:
* - When CORS_ORIGIN is set: allow only those origins (comma-separated)
* - When unset in production: warn and allow no origins (restrictive default)
* - When unset in production: throws an error at startup (hard failure)
* - When unset outside production: default to localhost:5173 (dev convenience)
*/

Expand All @@ -28,15 +28,14 @@ export interface CorsConfig {
* Returns a ready-to-use Hono `cors()` middleware configured for the current
* environment:
* - If `corsOriginEnv` is set (comma-separated), only those origins are allowed.
* - If `corsOriginEnv` is unset in production, a warning is logged and an empty
* origin list is used (blocks all cross-origin requests).
* - If `corsOriginEnv` is unset in production, throws an `Error` to crash the
* process at startup with a clear, actionable message.
* - If `corsOriginEnv` is unset outside production, `http://localhost:5173` is
* used as a dev-friendly default.
*/
export function buildCorsMiddleware({
corsOriginEnv,
isProduction,
warn = console.warn,
}: CorsConfigOptions): ReturnType<typeof cors> {
const origins = corsOriginEnv
?.split(',')
Expand All @@ -48,12 +47,11 @@ export function buildCorsMiddleware({
}

if (isProduction) {
warn(
'[Dashboard] WARNING: CORS_ORIGIN is not set in production. ' +
'Using restrictive default (no origins allowed). ' +
throw new Error(
'[Dashboard] CORS_ORIGIN is not set. ' +
'This is required in production. ' +
'Set CORS_ORIGIN to your frontend URL (e.g., https://dashboard.example.com).',
);
return cors({ origin: [], credentials: true });
}

// Development default
Expand Down
46 changes: 14 additions & 32 deletions tests/unit/utils/corsConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,40 +77,22 @@ describe('buildCorsMiddleware', () => {
});

describe('when CORS_ORIGIN is not set AND NODE_ENV=production', () => {
it('logs a warning at startup', () => {
const warn = vi.fn();
buildCorsMiddleware({
corsOriginEnv: undefined,
isProduction: true,
warn,
});

expect(warn).toHaveBeenCalledOnce();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('CORS_ORIGIN is not set in production'),
);
});

it('blocks all cross-origin requests (empty origin list)', async () => {
const middleware = buildCorsMiddleware({
corsOriginEnv: undefined,
isProduction: true,
warn: vi.fn(),
});

const res = await fetchWithOrigin(middleware, 'https://any-origin.example.com');
expect(res.headers.get('Access-Control-Allow-Origin')).toBeNull();
it('throws an error at startup', () => {
expect(() =>
buildCorsMiddleware({
corsOriginEnv: undefined,
isProduction: true,
}),
).toThrowError(/CORS_ORIGIN is not set/);
});

it('also blocks localhost when in production without CORS_ORIGIN', async () => {
const middleware = buildCorsMiddleware({
corsOriginEnv: undefined,
isProduction: true,
warn: vi.fn(),
});

const res = await fetchWithOrigin(middleware, 'http://localhost:5173');
expect(res.headers.get('Access-Control-Allow-Origin')).toBeNull();
it('throws an error with an actionable message', () => {
expect(() =>
buildCorsMiddleware({
corsOriginEnv: undefined,
isProduction: true,
}),
).toThrowError(/Set CORS_ORIGIN to your frontend URL/);
});
});

Expand Down
Loading