diff --git a/apps/api/src/trigger/vendor/vendor-risk-assessment/url-validation.spec.ts b/apps/api/src/trigger/vendor/vendor-risk-assessment/url-validation.spec.ts index 129901c63..ab46d9fe7 100644 --- a/apps/api/src/trigger/vendor/vendor-risk-assessment/url-validation.spec.ts +++ b/apps/api/src/trigger/vendor/vendor-risk-assessment/url-validation.spec.ts @@ -81,8 +81,19 @@ describe('extractVendorDomain', () => { expect(extractVendorDomain('')).toBe(null); }); - it('preserves subdomains other than www', () => { - expect(extractVendorDomain('https://trust.wix.com')).toBe('trust.wix.com'); + it('extracts root domain from subdomain websites', () => { + expect(extractVendorDomain('https://app.slack.com')).toBe('slack.com'); + expect(extractVendorDomain('https://trust.wix.com')).toBe('wix.com'); + expect(extractVendorDomain('https://dashboard.stripe.com')).toBe('stripe.com'); + }); + + it('extracts root domain from multi-level subdomains', () => { + expect(extractVendorDomain('https://app.us.slack.com')).toBe('slack.com'); + }); + + it('handles two-part TLDs correctly', () => { + expect(extractVendorDomain('https://app.example.co.uk')).toBe('example.co.uk'); + expect(extractVendorDomain('https://www.example.com.au')).toBe('example.com.au'); }); }); @@ -116,4 +127,20 @@ describe('validateVendorUrl', () => { validateVendorUrl('https://trust.wix.com', 'wix.com', 'trust'), ).toBe('https://trust.wix.com/'); }); + + it('accepts parent domain URLs when vendor website is a subdomain', () => { + // Vendor website is app.slack.com → domain extracts to slack.com + // Privacy policy at slack.com/privacy should be accepted + expect( + validateVendorUrl('https://slack.com/privacy', 'slack.com', 'privacy'), + ).toBe('https://slack.com/privacy'); + }); + + it('accepts sibling subdomain URLs', () => { + // Vendor website is app.slack.com → domain extracts to slack.com + // Trust center at trust.slack.com should be accepted + expect( + validateVendorUrl('https://trust.slack.com', 'slack.com', 'trust'), + ).toBe('https://trust.slack.com/'); + }); }); diff --git a/apps/api/src/trigger/vendor/vendor-risk-assessment/url-validation.ts b/apps/api/src/trigger/vendor/vendor-risk-assessment/url-validation.ts index c49f2224a..ddf755e06 100644 --- a/apps/api/src/trigger/vendor/vendor-risk-assessment/url-validation.ts +++ b/apps/api/src/trigger/vendor/vendor-risk-assessment/url-validation.ts @@ -1,4 +1,5 @@ import { logger } from '@trigger.dev/sdk'; +import { getDomain } from 'tldts'; /** * Checks whether a URL belongs to the given vendor domain (including subdomains). @@ -20,7 +21,9 @@ export function isUrlFromVendorDomain( } /** - * Extracts the vendor domain from a website URL, stripping www. prefix. + * Extracts the root registrable domain from a vendor website URL. + * Strips subdomains (including www.) to return the base domain. + * For example, "https://app.slack.com" → "slack.com". * Returns null if the URL is invalid. */ export function extractVendorDomain( @@ -30,7 +33,8 @@ export function extractVendorDomain( const urlObj = new URL( /^https?:\/\//i.test(website) ? website : `https://${website}`, ); - return urlObj.hostname.toLowerCase().replace(/^www\./, ''); + const domain = getDomain(urlObj.hostname); + return domain?.toLowerCase() ?? null; } catch { return null; }