Skip to content

Commit 35a7062

Browse files
committed
chore: merge main into release for new releases
2 parents 19cbd14 + 1b68e6a commit 35a7062

File tree

15 files changed

+342
-88
lines changed

15 files changed

+342
-88
lines changed

apps/api/src/trust-portal/dto/domain-status.dto.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,11 @@ export class DomainStatusResponseDto {
4444
required: false,
4545
})
4646
verification?: DomainVerificationDto[];
47+
48+
@ApiProperty({
49+
description: 'The recommended CNAME target for this domain from Vercel',
50+
example: 'cname.vercel-dns.com',
51+
required: false,
52+
})
53+
cnameTarget?: string;
4754
}

apps/api/src/trust-portal/trust-portal.service.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ interface VercelDomainResponse {
4747
verification?: VercelDomainVerification[];
4848
}
4949

50+
interface VercelRecommendedCNAME {
51+
rank: number;
52+
value: string;
53+
}
54+
55+
interface VercelDomainConfigResponse {
56+
configuredBy?: 'CNAME' | 'A' | 'http' | 'dns-01' | null;
57+
misconfigured: boolean;
58+
recommendedCNAME?: VercelRecommendedCNAME[];
59+
}
60+
5061
@Injectable()
5162
export class TrustPortalService {
5263
private readonly logger = new Logger(TrustPortalService.name);
@@ -170,16 +181,33 @@ export class TrustPortalService {
170181

171182
// Get domain information including verification status
172183
// Vercel API endpoint: GET /v9/projects/{projectId}/domains/{domain}
173-
const response = await this.vercelApi.get<VercelDomainResponse>(
174-
`/v9/projects/${process.env.TRUST_PORTAL_PROJECT_ID}/domains/${domain}`,
175-
{
176-
params: {
177-
teamId: process.env.VERCEL_TEAM_ID,
184+
const [domainResponse, configResponse] = await Promise.all([
185+
this.vercelApi.get<VercelDomainResponse>(
186+
`/v9/projects/${process.env.TRUST_PORTAL_PROJECT_ID}/domains/${domain}`,
187+
{
188+
params: {
189+
teamId: process.env.VERCEL_TEAM_ID,
190+
},
178191
},
179-
},
180-
);
181-
182-
const domainInfo = response.data;
192+
),
193+
// Get domain config to retrieve the actual CNAME target
194+
// Vercel API endpoint: GET /v6/domains/{domain}/config
195+
this.vercelApi
196+
.get<VercelDomainConfigResponse>(`/v6/domains/${domain}/config`, {
197+
params: {
198+
teamId: process.env.VERCEL_TEAM_ID,
199+
},
200+
})
201+
.catch((err) => {
202+
this.logger.warn(
203+
`Failed to get domain config for ${domain}: ${err.message}`,
204+
);
205+
return null;
206+
}),
207+
]);
208+
209+
const domainInfo = domainResponse.data;
210+
const configInfo = configResponse?.data;
183211

184212
const verification: DomainVerificationDto[] | undefined =
185213
domainInfo.verification?.map((v) => ({
@@ -189,10 +217,18 @@ export class TrustPortalService {
189217
reason: v.reason,
190218
}));
191219

220+
// Extract the CNAME target from the config response
221+
// Prefer rank=1 (preferred value), fallback to first available
222+
const recommendedCNAMEs = configInfo?.recommendedCNAME;
223+
const cnameTarget =
224+
recommendedCNAMEs?.find((c) => c.rank === 1)?.value ||
225+
recommendedCNAMEs?.[0]?.value;
226+
192227
return {
193228
domain: domainInfo.name,
194229
verified: domainInfo.verified ?? false,
195230
verification,
231+
cnameTarget,
196232
};
197233
} catch (error) {
198234
this.logger.error(

apps/app/src/app/(app)/[orgId]/components/AppShellWrapper.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
'use client';
22

3+
import { updateSidebarState } from '@/actions/sidebar';
34
import Chat from '@/components/ai/chat';
45
import { CheckoutCompleteDialog } from '@/components/dialogs/checkout-complete-dialog';
56
import { NotificationBell } from '@/components/notifications/notification-bell';
67
import { OrganizationSwitcher } from '@/components/organization-switcher';
7-
import { updateSidebarState } from '@/actions/sidebar';
88
import { SidebarProvider, useSidebar } from '@/context/sidebar-context';
99
import { authClient } from '@/utils/auth-client';
10-
import {
11-
CertificateCheck,
12-
Logout,
13-
Settings,
14-
} from '@carbon/icons-react';
10+
import { CertificateCheck, Logout, Settings } from '@carbon/icons-react';
1511
import {
1612
DropdownMenu,
1713
DropdownMenuContent,
@@ -40,16 +36,16 @@ import {
4036
HStack,
4137
Logo,
4238
Text,
43-
ThemeToggle,
39+
ThemeSwitcher,
4440
} from '@trycompai/design-system';
4541
import { useAction } from 'next-safe-action/hooks';
4642
import { useTheme } from 'next-themes';
4743
import Link from 'next/link';
4844
import { usePathname, useRouter } from 'next/navigation';
4945
import { Suspense, useCallback, useRef } from 'react';
5046
import { SettingsSidebar } from '../settings/components/SettingsSidebar';
51-
import { AppSidebar } from './AppSidebar';
5247
import { getAppShellSearchGroups } from './app-shell-search-groups';
48+
import { AppSidebar } from './AppSidebar';
5349
import { ConditionalOnboardingTracker } from './ConditionalOnboardingTracker';
5450

5551
interface AppShellWrapperProps {
@@ -94,7 +90,7 @@ function AppShellWrapperContent({
9490
isOnlyAuditor,
9591
user,
9692
}: AppShellWrapperContentProps) {
97-
const { resolvedTheme, setTheme } = useTheme();
93+
const { theme, resolvedTheme, setTheme } = useTheme();
9894
const pathname = usePathname();
9995
const router = useRouter();
10096
const { isCollapsed, setIsCollapsed } = useSidebar();
@@ -144,7 +140,11 @@ function AppShellWrapperContent({
144140
/>
145141
</Link>
146142
<span className="pl-3 pr-1 text-muted-foreground">/</span>
147-
<OrganizationSwitcher organizations={organizations} organization={organization} logoUrls={logoUrls} />
143+
<OrganizationSwitcher
144+
organizations={organizations}
145+
organization={organization}
146+
logoUrls={logoUrls}
147+
/>
148148
</HStack>
149149
}
150150
centerContent={<CommandSearch groups={searchGroups} placeholder="Search..." />}
@@ -182,10 +182,12 @@ function AppShellWrapperContent({
182182
<DropdownMenuSeparator />
183183
<div className="flex items-center justify-between px-2 py-1.5">
184184
<Text size="sm">Theme</Text>
185-
<ThemeToggle
185+
<ThemeSwitcher
186186
size="sm"
187-
isDark={resolvedTheme === 'dark'}
188-
onChange={(isDark) => setTheme(isDark ? 'dark' : 'light')}
187+
value={(theme ?? 'system') as 'light' | 'dark' | 'system'}
188+
defaultValue="system"
189+
onChange={(value) => setTheme(value)}
190+
showSystem
189191
/>
190192
</div>
191193
<DropdownMenuSeparator />

apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/check-dns-record.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ import { Vercel } from '@vercel/sdk';
77
import { revalidatePath, revalidateTag } from 'next/cache';
88
import { z } from 'zod';
99

10+
/**
11+
* Strict pattern to match known Vercel DNS CNAME targets.
12+
* Matches formats like:
13+
* - cname.vercel-dns.com
14+
* - 3a69a5bb27875189.vercel-dns-016.com
15+
* - With or without trailing dot
16+
*/
17+
const VERCEL_DNS_CNAME_PATTERN = /\.vercel-dns(-\d+)?\.com\.?$/i;
18+
19+
/**
20+
* Fallback pattern - more lenient, catches any vercel-dns variation.
21+
* Used if strict pattern fails, with logging for monitoring.
22+
*/
23+
const VERCEL_DNS_FALLBACK_PATTERN = /vercel-dns[^.]*\.com\.?$/i;
24+
1025
const checkDnsSchema = z.object({
1126
domain: z
1227
.string()
@@ -80,16 +95,31 @@ export const checkDnsRecordAction = authActionClient
8095
vercelVerification: true,
8196
},
8297
});
83-
const expectedCnameValue = 'cname.vercel-dns.com';
8498
const expectedTxtValue = `compai-domain-verification=${activeOrgId}`;
8599
const expectedVercelTxtValue = isVercelDomain?.vercelVerification;
86100

87101
let isCnameVerified = false;
88102

89103
if (cnameRecords) {
90-
isCnameVerified = cnameRecords.some(
91-
(record: { address: string }) => record.address.toLowerCase() === expectedCnameValue,
104+
// First try strict pattern
105+
isCnameVerified = cnameRecords.some((record: { address: string }) =>
106+
VERCEL_DNS_CNAME_PATTERN.test(record.address),
92107
);
108+
109+
// If strict fails, try fallback pattern (catches new Vercel patterns we haven't seen)
110+
if (!isCnameVerified) {
111+
const fallbackMatch = cnameRecords.find((record: { address: string }) =>
112+
VERCEL_DNS_FALLBACK_PATTERN.test(record.address),
113+
);
114+
115+
if (fallbackMatch) {
116+
console.warn(
117+
`[DNS Check] CNAME matched fallback pattern but not strict pattern. ` +
118+
`Address: ${fallbackMatch.address}. Consider updating VERCEL_DNS_CNAME_PATTERN.`,
119+
);
120+
isCnameVerified = true;
121+
}
122+
}
93123
}
94124

95125
let isTxtVerified = false;

apps/app/src/app/(app)/[orgId]/trust/portal-settings/actions/custom-domain.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export const customDomainAction = authActionClient
6969
}
7070
}
7171

72+
console.log(`Adding domain to Vercel project: ${domain}`);
73+
7274
const addDomainToProject = await vercel.projects.addProjectDomain({
7375
idOrName: env.TRUST_PORTAL_PROJECT_ID!,
7476
teamId: env.VERCEL_TEAM_ID!,
@@ -78,6 +80,8 @@ export const customDomainAction = authActionClient
7880
},
7981
});
8082

83+
console.log(`Vercel response for ${domain}:`, JSON.stringify(addDomainToProject, null, 2));
84+
8185
const isVercelDomain = addDomainToProject.verified === false;
8286

8387
// Store the verification details from Vercel if available
@@ -109,7 +113,25 @@ export const customDomainAction = authActionClient
109113
needsVerification: !domainVerified,
110114
};
111115
} catch (error) {
112-
console.error(error);
113-
throw new Error('Failed to update custom domain');
116+
console.error('Custom domain error:', error);
117+
118+
// Extract meaningful error message from Vercel SDK errors
119+
let errorMessage = 'Failed to update custom domain';
120+
121+
if (error instanceof Error) {
122+
// Check for Vercel API error responses
123+
const vercelError = error as Error & {
124+
body?: { error?: { code?: string; message?: string } };
125+
code?: string;
126+
};
127+
128+
if (vercelError.body?.error?.message) {
129+
errorMessage = vercelError.body.error.message;
130+
} else if (vercelError.message) {
131+
errorMessage = vercelError.message;
132+
}
133+
}
134+
135+
throw new Error(errorMessage);
114136
}
115137
});

apps/app/src/app/(app)/[orgId]/trust/portal-settings/components/TrustPortalDomain.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useDomain } from '@/hooks/use-domain';
3+
import { DEFAULT_CNAME_TARGET, useDomain } from '@/hooks/use-domain';
44
import { Button } from '@comp/ui/button';
55
import {
66
Card,
@@ -63,6 +63,13 @@ export function TrustPortalDomain({
6363
return null;
6464
}, [domainStatus]);
6565

66+
// Get the actual CNAME target from Vercel, with fallback
67+
// Normalize to include trailing dot for DNS record display
68+
const cnameTarget = useMemo(() => {
69+
const target = domainStatus?.data?.cnameTarget || DEFAULT_CNAME_TARGET;
70+
return target.endsWith('.') ? target : `${target}.`;
71+
}, [domainStatus?.data?.cnameTarget]);
72+
6673
useEffect(() => {
6774
const isCnameVerified = localStorage.getItem(`${initialDomain}-isCnameVerified`);
6875
const isTxtVerified = localStorage.getItem(`${initialDomain}-isTxtVerified`);
@@ -74,6 +81,11 @@ export function TrustPortalDomain({
7481

7582
const updateCustomDomain = useAction(customDomainAction, {
7683
onSuccess: (data) => {
84+
// Check if the action returned an error (e.g., domain already in use)
85+
if (data?.data?.success === false) {
86+
toast.error(data.data.error || 'Failed to update custom domain.');
87+
return;
88+
}
7789
toast.success('Custom domain update submitted, please verify your DNS records.');
7890
},
7991
onError: (error) => {
@@ -286,12 +298,12 @@ export function TrustPortalDomain({
286298
</td>
287299
<td>
288300
<div className="flex items-center justify-between gap-2">
289-
<span className="min-w-0 break-words">cname.vercel-dns.com.</span>
301+
<span className="min-w-0 break-words">{cnameTarget}</span>
290302
<Button
291303
variant="ghost"
292304
size="icon"
293305
type="button"
294-
onClick={() => handleCopy('cname.vercel-dns.com.', 'Value')}
306+
onClick={() => handleCopy(cnameTarget, 'Value')}
295307
className="h-6 w-6 shrink-0"
296308
>
297309
<ClipboardCopy className="h-4 w-4" />
@@ -411,12 +423,12 @@ export function TrustPortalDomain({
411423
<div>
412424
<div className="mb-1 font-medium">Value:</div>
413425
<div className="flex items-center justify-between gap-2">
414-
<span className="min-w-0 break-words">cname.vercel-dns.com.</span>
426+
<span className="min-w-0 break-words">{cnameTarget}</span>
415427
<Button
416428
variant="ghost"
417429
size="icon"
418430
type="button"
419-
onClick={() => handleCopy('cname.vercel-dns.com.', 'Value')}
431+
onClick={() => handleCopy(cnameTarget, 'Value')}
420432
className="h-6 w-6 shrink-0"
421433
>
422434
<ClipboardCopy className="h-4 w-4" />

0 commit comments

Comments
 (0)