Skip to content

Commit fc9c779

Browse files
authored
Merge pull request #2372 from trycompai/main
[comp] Production Deploy
2 parents e46977e + 0d54899 commit fc9c779

File tree

3 files changed

+81
-11
lines changed

3 files changed

+81
-11
lines changed

apps/api/src/auth/auth-server-origins.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,64 @@ describe('isStaticTrustedOrigin', () => {
115115
expect(mainTs).toContain("import { isTrustedOrigin } from './auth/auth.server'");
116116
});
117117
});
118+
119+
describe('getCustomDomains (structural)', () => {
120+
it('auth.server.ts should NOT filter by domainVerified in CORS domain query', () => {
121+
// Custom domains should be allowed for CORS as soon as they are configured
122+
// by an admin, not only after DNS verification completes. Vercel can serve
123+
// the trust portal before our domainVerified flag is set, causing CORS
124+
// failures on client-side API calls.
125+
const fs = require('fs');
126+
const path = require('path');
127+
const authServer = fs.readFileSync(
128+
path.join(__dirname, 'auth.server.ts'),
129+
'utf-8',
130+
) as string;
131+
132+
// Extract the getCustomDomains function body
133+
const fnMatch = authServer.match(
134+
/async function getCustomDomains[\s\S]*?^}/m,
135+
);
136+
expect(fnMatch).toBeTruthy();
137+
const fnBody = fnMatch![0];
138+
139+
// Must NOT require domainVerified — that flag lags behind Vercel's own verification
140+
expect(fnBody).not.toContain('domainVerified');
141+
142+
// Must still filter by published status
143+
expect(fnBody).toContain("status: 'published'");
144+
});
145+
146+
it('auth.server.ts getCustomDomains should have independent error handling for Redis and DB', () => {
147+
const fs = require('fs');
148+
const path = require('path');
149+
const authServer = fs.readFileSync(
150+
path.join(__dirname, 'auth.server.ts'),
151+
'utf-8',
152+
) as string;
153+
154+
const fnMatch = authServer.match(
155+
/async function getCustomDomains[\s\S]*?^}/m,
156+
);
157+
expect(fnMatch).toBeTruthy();
158+
const fnBody = fnMatch![0];
159+
160+
// Should have multiple try/catch blocks (Redis read, DB query, Redis write)
161+
const tryCatchCount = (fnBody.match(/\btry\s*\{/g) || []).length;
162+
expect(tryCatchCount).toBeGreaterThanOrEqual(3);
163+
});
164+
});
165+
166+
describe('originCheckMiddleware (structural)', () => {
167+
it('should exempt trust-access paths from origin validation', () => {
168+
const fs = require('fs');
169+
const path = require('path');
170+
const middleware = fs.readFileSync(
171+
path.join(__dirname, 'origin-check.middleware.ts'),
172+
'utf-8',
173+
) as string;
174+
175+
// Trust-access endpoints are public (no auth, no cookies) — no CSRF risk
176+
expect(middleware).toContain('/v1/trust-access');
177+
});
178+
});

apps/api/src/auth/auth.server.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,21 @@ const corsRedisClient = new Redis({
9494
});
9595

9696
async function getCustomDomains(): Promise<Set<string>> {
97+
// Try Redis cache first (non-fatal if Redis is unavailable)
9798
try {
98-
// Try Redis cache first
9999
const cached = await corsRedisClient.get<string[]>(CORS_DOMAINS_CACHE_KEY);
100100
if (cached) {
101101
return new Set(cached);
102102
}
103+
} catch (error) {
104+
console.error('[CORS] Redis cache read failed, falling back to DB:', error);
105+
}
103106

104-
// Cache miss — query DB and store in Redis
107+
// Cache miss or Redis unavailable — query DB
108+
try {
105109
const trusts = await db.trust.findMany({
106110
where: {
107111
domain: { not: null },
108-
domainVerified: true,
109112
status: 'published',
110113
},
111114
select: { domain: true },
@@ -115,13 +118,18 @@ async function getCustomDomains(): Promise<Set<string>> {
115118
.map((t) => t.domain)
116119
.filter((d): d is string => d !== null);
117120

118-
await corsRedisClient.set(CORS_DOMAINS_CACHE_KEY, domains, {
119-
ex: CORS_DOMAINS_CACHE_TTL_SECONDS,
120-
});
121+
// Best-effort cache update (don't lose DB results if Redis SET fails)
122+
try {
123+
await corsRedisClient.set(CORS_DOMAINS_CACHE_KEY, domains, {
124+
ex: CORS_DOMAINS_CACHE_TTL_SECONDS,
125+
});
126+
} catch {
127+
// Redis unavailable — continue without caching
128+
}
121129

122130
return new Set(domains);
123131
} catch (error) {
124-
console.error('[CORS] Failed to fetch custom domains:', error);
132+
console.error('[CORS] Failed to fetch custom domains from DB:', error);
125133
return new Set();
126134
}
127135
}
@@ -130,7 +138,7 @@ async function getCustomDomains(): Promise<Set<string>> {
130138
* Check if an origin is trusted. Checks (in order):
131139
* 1. Static trusted origins list
132140
* 2. *.trycomp.ai / *.trust.inc subdomains
133-
* 3. Verified custom domains from the DB (cached in Redis, TTL 5 min)
141+
* 3. Published custom domains from the DB (cached in Redis, TTL 5 min)
134142
*/
135143
export async function isTrustedOrigin(origin: string): Promise<boolean> {
136144
if (isStaticTrustedOrigin(origin)) {

apps/api/src/auth/origin-check.middleware.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
88
* These are called by external services that don't send browser Origin headers.
99
*/
1010
const EXEMPT_PATH_PREFIXES = [
11-
'/api/auth', // better-auth handles its own CSRF
12-
'/v1/health', // health check
13-
'/api/docs', // swagger
11+
'/api/auth', // better-auth handles its own CSRF
12+
'/v1/health', // health check
13+
'/api/docs', // swagger
14+
'/v1/trust-access', // public trust portal endpoints (no auth, no cookies)
1415
];
1516

1617
/**

0 commit comments

Comments
 (0)