From e967fabb468d8bc2e81b847950c7a14c40d9e61a Mon Sep 17 00:00:00 2001 From: Joel Fernandes <150249488+jfernsio@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:16:56 +0000 Subject: [PATCH] feat: add Cors config and origins --- backend/README.md | 4 ++-- backend/src/index.js | 8 +++++++- backend/src/index.test.js | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/backend/README.md b/backend/README.md index c6f07f5..9debbf1 100644 --- a/backend/README.md +++ b/backend/README.md @@ -13,8 +13,8 @@ npm run dev ## Environment - `PORT`: Server port (default `3001`) -- `CORS_ALLOWED_ORIGINS`: Comma-separated allowed origins for CORS (example: `https://app.example.com,https://admin.example.com`) -- `CORS_ORIGIN`: Legacy single-origin CORS setting (fallback when `CORS_ALLOWED_ORIGINS` is not set) +- `CORS_ALLOWED_ORIGINS`: Comma-separated allowed origins for CORS (example: `https://app.example.com,https://admin.example.com`). If this is not set in production, CORS defaults to a safe deny-all for cross-origin requests. +- `CORS_ORIGIN`: Legacy single-origin CORS setting (fallback when `CORS_ALLOWED_ORIGINS` is not set). In development, if neither is set, defaults to `http://localhost:5173`. - `STELLAR_NETWORK`: `testnet` or `mainnet` - `SOROBAN_RPC_URL`: Soroban RPC URL exposed in API metadata - `REWARDS_CONTRACT_ID`: Optional rewards contract ID exposed by `/api/v1/config` diff --git a/backend/src/index.js b/backend/src/index.js index 726a1c6..994cbdc 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -124,8 +124,14 @@ function nextCampaignId(campaigns) { export function createApp(options = {}) { const apiKey = options.apiKey ?? process.env.TRIVELA_API_KEY ?? ''; - const corsAllowedOrigins = + const corsOriginConfig = options.corsAllowedOrigins ?? process.env.CORS_ALLOWED_ORIGINS ?? process.env.CORS_ORIGIN; + const corsAllowedOrigins = + typeof corsOriginConfig === 'string' && corsOriginConfig.trim().length > 0 + ? corsOriginConfig + : process.env.NODE_ENV === 'production' + ? '' + : 'http://localhost:5173'; const stellarNetwork = options.stellarNetwork ?? process.env.STELLAR_NETWORK ?? 'testnet'; const sorobanRpcUrl = options.sorobanRpcUrl ?? process.env.SOROBAN_RPC_URL ?? DEFAULT_RPC_URL; const rewardsContractId = readOptionalConfigValue(options, 'REWARDS_CONTRACT_ID'); diff --git a/backend/src/index.test.js b/backend/src/index.test.js index ac10fd2..039b5fb 100644 --- a/backend/src/index.test.js +++ b/backend/src/index.test.js @@ -140,6 +140,45 @@ test('/health/rpc returns 503 when the Soroban RPC health check fails', async () } }); +test('CORS allows configured origins and rejects others', async () => { + const { server, baseUrl } = await startTestServer({ + corsAllowedOrigins: 'https://app.example.com, https://admin.example.com', + }); + + try { + const allowedRes = await fetch(`${baseUrl}/api/v1`, { + headers: { origin: 'https://app.example.com' }, + }); + assert.equal(allowedRes.status, 200); + assert.equal(allowedRes.headers.get('access-control-allow-origin'), 'https://app.example.com'); + + const deniedRes = await fetch(`${baseUrl}/api/v1`, { + headers: { origin: 'https://evil.example' }, + }); + assert.equal(deniedRes.status, 200); + assert.equal(deniedRes.headers.get('access-control-allow-origin'), null); + } finally { + await stopTestServer(server); + } +}); + +test('CORS production default is safe (no open origins)', async () => { + const originalNodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + const { server, baseUrl } = await startTestServer({}); + + try { + const response = await fetch(`${baseUrl}/api/v1`, { + headers: { origin: 'http://localhost:5173' }, + }); + assert.equal(response.status, 200); + assert.equal(response.headers.get('access-control-allow-origin'), null); + } finally { + await stopTestServer(server); + process.env.NODE_ENV = originalNodeEnv; + } +}); + test('POST /api/campaigns creates a new campaign and returns it', async () => { const { server, baseUrl } = await startTestServer();