@@ -183,7 +183,7 @@ export class TrustPortalService {
183183 // Vercel API endpoint: GET /v9/projects/{projectId}/domains/{domain}
184184 const [ domainResponse , configResponse ] = await Promise . all ( [
185185 this . vercelApi . get < VercelDomainResponse > (
186- `/v9/projects/${ process . env . TRUST_PORTAL_PROJECT_ID } /domains/${ domain } ` ,
186+ `/v9/projects/${ process . env . TRUST_PORTAL_PROJECT_ID } /domains/${ TrustPortalService . safeDomainPath ( domain ) } ` ,
187187 {
188188 params : {
189189 teamId : process . env . VERCEL_TEAM_ID ,
@@ -193,7 +193,7 @@ export class TrustPortalService {
193193 // Get domain config to retrieve the actual CNAME target
194194 // Vercel API endpoint: GET /v6/domains/{domain}/config
195195 this . vercelApi
196- . get < VercelDomainConfigResponse > ( `/v6/domains/${ domain } /config` , {
196+ . get < VercelDomainConfigResponse > ( `/v6/domains/${ TrustPortalService . safeDomainPath ( domain ) } /config` , {
197197 params : {
198198 teamId : process . env . VERCEL_TEAM_ID ,
199199 } ,
@@ -752,6 +752,20 @@ export class TrustPortalService {
752752 ? currentTrust . domainVerified
753753 : false ;
754754
755+ // Remove old domain from Vercel if switching to a different one
756+ if ( currentTrust ?. domain && currentTrust . domain !== domain ) {
757+ try {
758+ await this . vercelApi . delete (
759+ `/v9/projects/${ projectId } /domains/${ TrustPortalService . safeDomainPath ( currentTrust . domain ) } ` ,
760+ { params : { teamId } } ,
761+ ) ;
762+ } catch ( error ) {
763+ this . logger . warn (
764+ `Failed to remove old domain ${ currentTrust . domain } from Vercel: ${ error } ` ,
765+ ) ;
766+ }
767+ }
768+
755769 // Check if domain already exists on the Vercel project
756770 const existingDomainsResp = await this . vercelApi . get (
757771 `/v9/projects/${ projectId } /domains` ,
@@ -761,22 +775,54 @@ export class TrustPortalService {
761775 const existingDomains : Array < { name : string } > =
762776 existingDomainsResp . data ?. domains ?? [ ] ;
763777
764- if ( existingDomains . some ( ( d ) => d . name === domain ) ) {
765- const domainOwner = await db . trust . findUnique ( {
766- where : { organizationId, domain } ,
778+ const alreadyOnProject = existingDomains . some ( ( d ) => d . name === domain ) ;
779+
780+ if ( alreadyOnProject ) {
781+ const domainOwner = await db . trust . findFirst ( {
782+ where : { domain, organizationId : { not : organizationId } } ,
783+ select : { organizationId : true } ,
767784 } ) ;
768785
769- if ( ! domainOwner || domainOwner . organizationId === organizationId ) {
770- await this . vercelApi . delete (
771- `/v9/projects/${ projectId } /domains/${ domain } ` ,
772- { params : { teamId } } ,
773- ) ;
774- } else {
786+ if ( domainOwner ) {
775787 return {
776788 success : false ,
777789 error : 'Domain is already in use by another organization' ,
778790 } ;
779791 }
792+
793+ // Domain already on Vercel for this org — fetch current status
794+ // instead of deleting and re-adding (which regenerates verification tokens)
795+ const statusResp = await this . vercelApi . get (
796+ `/v9/projects/${ projectId } /domains/${ TrustPortalService . safeDomainPath ( domain ) } ` ,
797+ { params : { teamId } } ,
798+ ) ;
799+
800+ const statusData = statusResp . data ;
801+ const isVercelDomain = statusData . verified === false ;
802+ const vercelVerification =
803+ statusData . verification ?. [ 0 ] ?. value || null ;
804+
805+ await db . trust . upsert ( {
806+ where : { organizationId } ,
807+ update : {
808+ domain,
809+ domainVerified,
810+ isVercelDomain,
811+ vercelVerification,
812+ } ,
813+ create : {
814+ organizationId,
815+ domain,
816+ domainVerified : false ,
817+ isVercelDomain,
818+ vercelVerification,
819+ } ,
820+ } ) ;
821+
822+ return {
823+ success : true ,
824+ needsVerification : ! domainVerified ,
825+ } ;
780826 }
781827
782828 this . logger . log ( `Adding domain to Vercel project: ${ domain } ` ) ;
@@ -891,6 +937,11 @@ export class TrustPortalService {
891937 }
892938 }
893939
940+ /** Encode a validated domain for safe use in URL path segments. */
941+ private static safeDomainPath ( domain : string ) : string {
942+ return encodeURIComponent ( domain ) ;
943+ }
944+
894945 /**
895946 * DNS CNAME patterns for Vercel verification.
896947 */
@@ -902,20 +953,30 @@ export class TrustPortalService {
902953 async checkDnsRecords ( organizationId : string , domain : string ) {
903954 this . validateDomain ( domain ) ;
904955
956+ // Verify the domain belongs to this organization
957+ const trustRecord = await db . trust . findUnique ( {
958+ where : { organizationId } ,
959+ } ) ;
960+ if ( ! trustRecord || trustRecord . domain !== domain ) {
961+ throw new BadRequestException (
962+ 'Domain does not match the configured domain for this organization' ,
963+ ) ;
964+ }
965+
905966 const rootDomain = domain . split ( '.' ) . slice ( - 2 ) . join ( '.' ) ;
906967
907968 const [ cnameResp , txtResp , vercelTxtResp ] = await Promise . all ( [
908969 axios
909- . get ( `https://networkcalc.com/api/dns/lookup/${ domain } ` )
970+ . get ( `https://networkcalc.com/api/dns/lookup/${ TrustPortalService . safeDomainPath ( domain ) } ` )
910971 . catch ( ( ) => null ) ,
911972 axios
912973 . get (
913- `https://networkcalc.com/api/dns/lookup/${ rootDomain } ?type=TXT` ,
974+ `https://networkcalc.com/api/dns/lookup/${ TrustPortalService . safeDomainPath ( rootDomain ) } ?type=TXT` ,
914975 )
915976 . catch ( ( ) => null ) ,
916977 axios
917978 . get (
918- `https://networkcalc.com/api/dns/lookup/_vercel.${ rootDomain } ?type=TXT` ,
979+ `https://networkcalc.com/api/dns/lookup/_vercel.${ TrustPortalService . safeDomainPath ( rootDomain ) } ?type=TXT` ,
919980 )
920981 . catch ( ( ) => null ) ,
921982 ] ) ;
@@ -937,13 +998,45 @@ export class TrustPortalService {
937998 const txtRecords = txtResp . data ?. records ?. TXT ;
938999 const vercelTxtRecords = vercelTxtResp ?. data ?. records ?. TXT ;
9391000
940- const trustRecord = await db . trust . findUnique ( {
941- where : { organizationId, domain } ,
942- select : { isVercelDomain : true , vercelVerification : true } ,
943- } ) ;
1001+ // Fetch fresh verification state from Vercel instead of relying on
1002+ // potentially stale DB values (tokens change if domain was re-added).
1003+ let liveIsVercelDomain = false ;
1004+ let liveVercelVerification : string | null = null ;
1005+
1006+ if ( process . env . TRUST_PORTAL_PROJECT_ID && process . env . VERCEL_TEAM_ID ) {
1007+ try {
1008+ const vercelStatusResp = await this . vercelApi . get (
1009+ `/v9/projects/${ process . env . TRUST_PORTAL_PROJECT_ID } /domains/${ TrustPortalService . safeDomainPath ( domain ) } ` ,
1010+ { params : { teamId : process . env . VERCEL_TEAM_ID } } ,
1011+ ) ;
1012+ const vercelData = vercelStatusResp . data ;
1013+ liveIsVercelDomain = vercelData . verified === false ;
1014+ liveVercelVerification =
1015+ vercelData . verification ?. [ 0 ] ?. value || null ;
1016+
1017+ // Sync DB with live Vercel state
1018+ await db . trust . update ( {
1019+ where : { organizationId } ,
1020+ data : {
1021+ isVercelDomain : liveIsVercelDomain ,
1022+ vercelVerification : liveVercelVerification ,
1023+ } ,
1024+ } ) ;
1025+ } catch ( error ) {
1026+ this . logger . warn (
1027+ `Failed to fetch live Vercel status for ${ domain } , falling back to DB: ${ error } ` ,
1028+ ) ;
1029+ const trustRecord = await db . trust . findUnique ( {
1030+ where : { organizationId, domain } ,
1031+ select : { isVercelDomain : true , vercelVerification : true } ,
1032+ } ) ;
1033+ liveIsVercelDomain = trustRecord ?. isVercelDomain === true ;
1034+ liveVercelVerification = trustRecord ?. vercelVerification ?? null ;
1035+ }
1036+ }
9441037
9451038 const expectedTxtValue = `compai-domain-verification=${ organizationId } ` ;
946- const expectedVercelTxtValue = trustRecord ?. vercelVerification ;
1039+ const expectedVercelTxtValue = liveVercelVerification ;
9471040
9481041 // Check CNAME
9491042 let isCnameVerified = false ;
@@ -993,7 +1086,7 @@ export class TrustPortalService {
9931086 } ) ;
9941087 }
9951088
996- const requiresVercelTxt = trustRecord ?. isVercelDomain === true ;
1089+ const requiresVercelTxt = liveIsVercelDomain ;
9971090 const isVerified =
9981091 isCnameVerified &&
9991092 isTxtVerified &&
@@ -1010,34 +1103,50 @@ export class TrustPortalService {
10101103 } ;
10111104 }
10121105
1013- await db . trust . upsert ( {
1014- where : { organizationId, domain } ,
1015- update : { domainVerified : true , status : 'published' } ,
1016- create : {
1017- organizationId,
1018- domain,
1019- status : 'published' ,
1020- } ,
1021- } ) ;
1022-
10231106 // Trigger Vercel to re-verify the domain so it provisions SSL and starts serving.
1024- // Without this, Vercel doesn't know DNS has been configured and the domain stays inactive
1025- // (previously required CS to manually click "Refresh" in Vercel dashboard).
1107+ let vercelVerified = false ;
10261108 if ( process . env . TRUST_PORTAL_PROJECT_ID && process . env . VERCEL_TEAM_ID ) {
10271109 try {
1028- await this . vercelApi . post (
1029- `/v9/projects/${ process . env . TRUST_PORTAL_PROJECT_ID } /domains/${ domain } /verify` ,
1110+ const verifyResp = await this . vercelApi . post (
1111+ `/v9/projects/${ process . env . TRUST_PORTAL_PROJECT_ID } /domains/${ TrustPortalService . safeDomainPath ( domain ) } /verify` ,
10301112 { } ,
10311113 { params : { teamId : process . env . VERCEL_TEAM_ID } } ,
10321114 ) ;
1115+ vercelVerified = verifyResp . data ?. verified === true ;
10331116 } catch ( error ) {
1034- // Non-fatal — domain is verified on our side, Vercel will eventually pick it up
10351117 this . logger . warn (
10361118 `Failed to trigger Vercel domain verification for ${ domain } : ${ error } ` ,
10371119 ) ;
10381120 }
10391121 }
10401122
1123+ // For cross-account domains (liveIsVercelDomain=true), Vercel must confirm
1124+ // the _vercel TXT record before the domain will serve traffic.
1125+ // For same-account domains, DNS verification is sufficient — Vercel will
1126+ // pick up the CNAME on its own, so don't block on the verify response.
1127+ const domainFullyVerified = requiresVercelTxt
1128+ ? vercelVerified
1129+ : true ;
1130+
1131+ await db . trust . update ( {
1132+ where : { organizationId } ,
1133+ data : {
1134+ domainVerified : domainFullyVerified ,
1135+ ...( domainFullyVerified ? { status : 'published' as const } : { } ) ,
1136+ } ,
1137+ } ) ;
1138+
1139+ if ( ! domainFullyVerified ) {
1140+ return {
1141+ success : false ,
1142+ isCnameVerified,
1143+ isTxtVerified,
1144+ isVercelTxtVerified,
1145+ error :
1146+ 'DNS records verified but Vercel has not yet confirmed domain ownership. Please ensure the _vercel TXT record is correctly configured and try again.' ,
1147+ } ;
1148+ }
1149+
10411150 return {
10421151 success : true ,
10431152 isCnameVerified,
0 commit comments