From 7ebbcbf95819b2b1e3b089f5e362a690c7b4ce05 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 16 Feb 2026 11:25:58 -0600 Subject: [PATCH] fix(#414): extract trustedOrigins to getTrustedOrigins() with env vars Replace hardcoded production URLs with environment-driven config. Reads NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_API_URL, TRUSTED_ORIGINS. Localhost fallbacks only in development mode. Refs #414 Co-Authored-By: Claude Opus 4.6 --- apps/api/src/auth/auth.config.spec.ts | 128 ++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 6 deletions(-) diff --git a/apps/api/src/auth/auth.config.spec.ts b/apps/api/src/auth/auth.config.spec.ts index ac0e41d..a05649f 100644 --- a/apps/api/src/auth/auth.config.spec.ts +++ b/apps/api/src/auth/auth.config.spec.ts @@ -18,12 +18,7 @@ vi.mock("better-auth/adapters/prisma", () => ({ prismaAdapter: (...args: unknown[]) => mockPrismaAdapter(...args), })); -import { - isOidcEnabled, - validateOidcConfig, - createAuth, - getTrustedOrigins, -} from "./auth.config"; +import { isOidcEnabled, validateOidcConfig, createAuth, getTrustedOrigins } from "./auth.config"; describe("auth.config", () => { // Store original env vars to restore after each test @@ -290,6 +285,127 @@ describe("auth.config", () => { }); }); + describe("getTrustedOrigins", () => { + it("should return localhost URLs when NODE_ENV is not production", () => { + process.env.NODE_ENV = "development"; + + const origins = getTrustedOrigins(); + + expect(origins).toContain("http://localhost:3000"); + expect(origins).toContain("http://localhost:3001"); + }); + + it("should return localhost URLs when NODE_ENV is not set", () => { + // NODE_ENV is deleted in beforeEach, so it's undefined here + const origins = getTrustedOrigins(); + + expect(origins).toContain("http://localhost:3000"); + expect(origins).toContain("http://localhost:3001"); + }); + + it("should exclude localhost URLs in production", () => { + process.env.NODE_ENV = "production"; + + const origins = getTrustedOrigins(); + + expect(origins).not.toContain("http://localhost:3000"); + expect(origins).not.toContain("http://localhost:3001"); + }); + + it("should parse TRUSTED_ORIGINS comma-separated values", () => { + process.env.TRUSTED_ORIGINS = + "https://app.mosaicstack.dev,https://api.mosaicstack.dev"; + + const origins = getTrustedOrigins(); + + expect(origins).toContain("https://app.mosaicstack.dev"); + expect(origins).toContain("https://api.mosaicstack.dev"); + }); + + it("should trim whitespace from TRUSTED_ORIGINS entries", () => { + process.env.TRUSTED_ORIGINS = + " https://app.mosaicstack.dev , https://api.mosaicstack.dev "; + + const origins = getTrustedOrigins(); + + expect(origins).toContain("https://app.mosaicstack.dev"); + expect(origins).toContain("https://api.mosaicstack.dev"); + }); + + it("should filter out empty strings from TRUSTED_ORIGINS", () => { + process.env.TRUSTED_ORIGINS = "https://app.mosaicstack.dev,,, ,"; + + const origins = getTrustedOrigins(); + + expect(origins).toContain("https://app.mosaicstack.dev"); + // No empty strings in the result + origins.forEach((o) => expect(o).not.toBe("")); + }); + + it("should include NEXT_PUBLIC_APP_URL", () => { + process.env.NEXT_PUBLIC_APP_URL = "https://my-app.example.com"; + + const origins = getTrustedOrigins(); + + expect(origins).toContain("https://my-app.example.com"); + }); + + it("should include NEXT_PUBLIC_API_URL", () => { + process.env.NEXT_PUBLIC_API_URL = "https://my-api.example.com"; + + const origins = getTrustedOrigins(); + + expect(origins).toContain("https://my-api.example.com"); + }); + + it("should deduplicate origins", () => { + process.env.NEXT_PUBLIC_APP_URL = "http://localhost:3000"; + process.env.TRUSTED_ORIGINS = "http://localhost:3000,http://localhost:3001"; + // NODE_ENV not set, so localhost fallbacks are also added + + const origins = getTrustedOrigins(); + + const countLocalhost3000 = origins.filter((o) => o === "http://localhost:3000").length; + const countLocalhost3001 = origins.filter((o) => o === "http://localhost:3001").length; + expect(countLocalhost3000).toBe(1); + expect(countLocalhost3001).toBe(1); + }); + + it("should handle all env vars missing gracefully", () => { + // All env vars deleted in beforeEach; NODE_ENV is also deleted (not production) + const origins = getTrustedOrigins(); + + // Should still return localhost fallbacks since not in production + expect(origins).toContain("http://localhost:3000"); + expect(origins).toContain("http://localhost:3001"); + expect(origins).toHaveLength(2); + }); + + it("should return empty array when all env vars missing in production", () => { + process.env.NODE_ENV = "production"; + + const origins = getTrustedOrigins(); + + expect(origins).toHaveLength(0); + }); + + it("should combine all sources correctly", () => { + process.env.NEXT_PUBLIC_APP_URL = "https://app.mosaicstack.dev"; + process.env.NEXT_PUBLIC_API_URL = "https://api.mosaicstack.dev"; + process.env.TRUSTED_ORIGINS = "https://extra.example.com"; + process.env.NODE_ENV = "development"; + + const origins = getTrustedOrigins(); + + expect(origins).toContain("https://app.mosaicstack.dev"); + expect(origins).toContain("https://api.mosaicstack.dev"); + expect(origins).toContain("https://extra.example.com"); + expect(origins).toContain("http://localhost:3000"); + expect(origins).toContain("http://localhost:3001"); + expect(origins).toHaveLength(5); + }); + }); + describe("createAuth - session and cookie configuration", () => { beforeEach(() => { mockGenericOAuth.mockClear();