diff --git a/Docs/vercel-deploy.md b/Docs/vercel-deploy.md index cffa9f2..d8e4527 100644 --- a/Docs/vercel-deploy.md +++ b/Docs/vercel-deploy.md @@ -28,9 +28,14 @@ ZCore uses two Vercel projects in this monorepo. - `DATABASE_URL` — managed MySQL (#15) - `ADMIN_SECRET` — strong random secret - `STELLAR_NETWORK=testnet` +- `CORS_ORIGINS=https://dapp-zcore.vercel.app,https://zcore-xi.vercel.app` - `SCORE_REGISTRY_CONTRACT_ID` — optional (#16) - `ORACLE_SECRET_KEY` — optional +`CORS_ORIGINS` is a comma-separated allowlist of browser origins that may call +the API with credentials. Keep `http://localhost:3001` in local `.env` files, +and set only production dapp or partner origins in Vercel. + ## Manual deploy (fallback) ```bash diff --git a/Server/src/config/__tests__/cors.config.test.ts b/Server/src/config/__tests__/cors.config.test.ts index 8fc00e5..0a00f0b 100644 --- a/Server/src/config/__tests__/cors.config.test.ts +++ b/Server/src/config/__tests__/cors.config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { parseAllowedOrigins } from "../../config/cors.config"; +import { getCorsOptions, parseAllowedOrigins } from "../../config/cors.config"; describe("cors.config", () => { it("parses comma-separated origins", () => { @@ -7,8 +7,79 @@ describe("cors.config", () => { expect(parseAllowedOrigins()).toEqual(["https://a.com", "https://b.com"]); }); + it("trims configured origins and ignores empty entries", () => { + process.env.CORS_ORIGINS = " https://dapp-zcore.vercel.app, ,https://zcore-xi.vercel.app "; + expect(parseAllowedOrigins()).toEqual([ + "https://dapp-zcore.vercel.app", + "https://zcore-xi.vercel.app", + ]); + }); + it("defaults to local dapp origin", () => { delete process.env.CORS_ORIGINS; expect(parseAllowedOrigins()).toContain("http://localhost:3001"); }); + + it("allows configured browser origins", () => + new Promise((resolve, reject) => { + process.env.CORS_ORIGINS = "https://dapp-zcore.vercel.app"; + const options = getCorsOptions(); + + if (typeof options.origin !== "function") { + reject(new Error("Expected function origin option")); + return; + } + + options.origin("https://dapp-zcore.vercel.app", (error, allowed) => { + try { + expect(error).toBeNull(); + expect(allowed).toBe(true); + resolve(); + } catch (assertionError) { + reject(assertionError); + } + }); + })); + + it("rejects browser origins outside the allowlist", () => + new Promise((resolve, reject) => { + process.env.CORS_ORIGINS = "https://dapp-zcore.vercel.app"; + const options = getCorsOptions(); + + if (typeof options.origin !== "function") { + reject(new Error("Expected function origin option")); + return; + } + + options.origin("https://evil.example", (error) => { + try { + expect(error).toBeInstanceOf(Error); + expect(error?.message).toBe("Origin https://evil.example not allowed by CORS"); + resolve(); + } catch (assertionError) { + reject(assertionError); + } + }); + })); + + it("allows server-to-server calls without an Origin header", () => + new Promise((resolve, reject) => { + process.env.CORS_ORIGINS = "https://dapp-zcore.vercel.app"; + const options = getCorsOptions(); + + if (typeof options.origin !== "function") { + reject(new Error("Expected function origin option")); + return; + } + + options.origin(undefined, (error, allowed) => { + try { + expect(error).toBeNull(); + expect(allowed).toBe(true); + resolve(); + } catch (assertionError) { + reject(assertionError); + } + }); + })); });