Skip to content

Commit cbcc3c5

Browse files
committed
fix: add CLIENT_URL and CHECKOUT_BASE_URL to CORS and trusted origins for self-hosted deployments
1 parent dfa53cc commit cbcc3c5

File tree

3 files changed

+76
-8
lines changed

3 files changed

+76
-8
lines changed

server/src/utils/auth.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,19 @@ export const auth = betterAuth({
6868
"https://*.useautumn.com",
6969
];
7070

71-
// Better Auth validates origins independently from app-level CORS.
72-
// Allow local multi-port setups for any non-production runtime.
71+
// Always trust CLIENT_URL and CHECKOUT_BASE_URL (required for self-hosted deployments)
72+
if (process.env.CLIENT_URL) {
73+
origins.push(process.env.CLIENT_URL);
74+
}
75+
if (process.env.CHECKOUT_BASE_URL) {
76+
origins.push(process.env.CHECKOUT_BASE_URL);
77+
}
78+
79+
// Allow local multi-port setups for any non-production runtime
7380
if (process.env.NODE_ENV !== "production") {
74-
// Add ports 3000-3010 for multiple instances
7581
for (let i = 0; i <= 10; i++) {
7682
origins.push(`http://localhost:${3000 + i}`);
7783
}
78-
79-
// Support multi-worktree dev with offset ports (e.g. localhost:3100)
80-
if (process.env.CLIENT_URL) {
81-
origins.push(process.env.CLIENT_URL);
82-
}
8384
}
8485

8586
return origins;

server/src/utils/corsOrigins.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ export const ALLOWED_ORIGINS = [
1919
/** Allow any localhost origin in dev for multi-worktree support */
2020
export const isAllowedOrigin = (origin: string): string | undefined => {
2121
if (ALLOWED_ORIGINS.includes(origin)) return origin;
22+
23+
// Allow CLIENT_URL and CHECKOUT_BASE_URL for self-hosted deployments
24+
if (process.env.CLIENT_URL && origin === process.env.CLIENT_URL) {
25+
return origin;
26+
}
27+
if (
28+
process.env.CHECKOUT_BASE_URL &&
29+
origin === process.env.CHECKOUT_BASE_URL
30+
) {
31+
return origin;
32+
}
33+
2234
if (
2335
process.env.NODE_ENV !== "production" &&
2436
/^https?:\/\/localhost:\d+$/.test(origin)

server/tests/unit/corsOrigins.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import { ALLOWED_ORIGINS, isAllowedOrigin } from "@/utils/corsOrigins.js";
33

44
describe("isAllowedOrigin", () => {
55
const originalNodeEnv = process.env.NODE_ENV;
6+
const originalClientUrl = process.env.CLIENT_URL;
7+
const originalCheckoutBaseUrl = process.env.CHECKOUT_BASE_URL;
68

79
afterEach(() => {
810
process.env.NODE_ENV = originalNodeEnv;
11+
process.env.CLIENT_URL = originalClientUrl;
12+
process.env.CHECKOUT_BASE_URL = originalCheckoutBaseUrl;
913
});
1014

1115
describe("production", () => {
@@ -63,4 +67,55 @@ describe("isAllowedOrigin", () => {
6367
expect(isAllowedOrigin("http://localhost:3000?x=1")).toBeUndefined();
6468
});
6569
});
70+
71+
describe("self-hosted env URLs", () => {
72+
test("allows CLIENT_URL in production", () => {
73+
process.env.NODE_ENV = "production";
74+
process.env.CLIENT_URL =
75+
"https://autumn-dashboard-production.up.railway.app";
76+
expect(
77+
isAllowedOrigin("https://autumn-dashboard-production.up.railway.app"),
78+
).toBe("https://autumn-dashboard-production.up.railway.app");
79+
});
80+
81+
test("allows CHECKOUT_BASE_URL in production", () => {
82+
process.env.NODE_ENV = "production";
83+
process.env.CHECKOUT_BASE_URL =
84+
"https://autumn-checkout-production.up.railway.app";
85+
expect(
86+
isAllowedOrigin("https://autumn-checkout-production.up.railway.app"),
87+
).toBe("https://autumn-checkout-production.up.railway.app");
88+
});
89+
90+
test("allows both CLIENT_URL and CHECKOUT_BASE_URL simultaneously", () => {
91+
process.env.NODE_ENV = "production";
92+
process.env.CLIENT_URL = "https://dashboard.mycompany.com";
93+
process.env.CHECKOUT_BASE_URL = "https://checkout.mycompany.com";
94+
expect(isAllowedOrigin("https://dashboard.mycompany.com")).toBe(
95+
"https://dashboard.mycompany.com",
96+
);
97+
expect(isAllowedOrigin("https://checkout.mycompany.com")).toBe(
98+
"https://checkout.mycompany.com",
99+
);
100+
});
101+
102+
test("still rejects unrelated origins when env URLs are set", () => {
103+
process.env.NODE_ENV = "production";
104+
process.env.CLIENT_URL = "https://dashboard.mycompany.com";
105+
process.env.CHECKOUT_BASE_URL = "https://checkout.mycompany.com";
106+
expect(isAllowedOrigin("https://evil.com")).toBeUndefined();
107+
expect(
108+
isAllowedOrigin("https://not-autumn.up.railway.app"),
109+
).toBeUndefined();
110+
});
111+
112+
test("rejects custom domains when env URLs are unset", () => {
113+
process.env.NODE_ENV = "production";
114+
delete process.env.CLIENT_URL;
115+
delete process.env.CHECKOUT_BASE_URL;
116+
expect(
117+
isAllowedOrigin("https://autumn-dashboard-production.up.railway.app"),
118+
).toBeUndefined();
119+
});
120+
});
66121
});

0 commit comments

Comments
 (0)