From 1b3d1caa2180d620d478bad7561f1ce6d576d417 Mon Sep 17 00:00:00 2001 From: anirudh5harma Date: Sun, 28 Jun 2026 02:53:54 +0530 Subject: [PATCH] Fix social URL host matching --- apps/web/lib/url-helpers.test.ts | 59 ++++++++++++++++++++++++++++++++ apps/web/lib/url-helpers.ts | 41 ++++++++++++++++++---- 2 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 apps/web/lib/url-helpers.test.ts diff --git a/apps/web/lib/url-helpers.test.ts b/apps/web/lib/url-helpers.test.ts new file mode 100644 index 000000000..8f6218682 --- /dev/null +++ b/apps/web/lib/url-helpers.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "bun:test" +import { + collectValidUrls, + isLinkedInProfileUrl, + isTwitterUrl, +} from "./url-helpers" + +describe("social URL detection", () => { + it("matches Twitter and X by hostname only", () => { + expect(isTwitterUrl("https://twitter.com/supermemoryai")).toBe(true) + expect(isTwitterUrl("https://mobile.twitter.com/supermemoryai")).toBe(true) + expect(isTwitterUrl("https://x.com/supermemoryai")).toBe(true) + expect(isTwitterUrl("x.com/supermemoryai")).toBe(true) + + expect(isTwitterUrl("https://exampletwitter.com/supermemoryai")).toBe(false) + expect(isTwitterUrl("https://notx.com/profile")).toBe(false) + expect(isTwitterUrl("https://example.com/twitter.com/supermemoryai")).toBe( + false, + ) + expect(isTwitterUrl("https://x.com.evil.example/supermemoryai")).toBe(false) + }) + + it("matches LinkedIn profile URLs by hostname and /in/ path", () => { + expect(isLinkedInProfileUrl("https://linkedin.com/in/supermemoryai")).toBe( + true, + ) + expect( + isLinkedInProfileUrl("https://www.linkedin.com/in/supermemoryai"), + ).toBe(true) + expect(isLinkedInProfileUrl("linkedin.com/in/supermemoryai")).toBe(true) + + expect( + isLinkedInProfileUrl( + "https://evil.example/linkedin.com/in/supermemoryai", + ), + ).toBe(false) + expect(isLinkedInProfileUrl("https://linkedin.com/company/acme")).toBe( + false, + ) + expect(isLinkedInProfileUrl("https://notlinkedin.com/in/person")).toBe( + false, + ) + expect(isLinkedInProfileUrl("https://linkedin.com/in")).toBe(false) + }) +}) + +describe("collectValidUrls", () => { + it("keeps non-Twitter domains that only contain x.com as text", () => { + expect(collectValidUrls("", ["https://notx.com/profile"])).toEqual([ + "https://notx.com/profile", + ]) + }) + + it("rejects LinkedIn profile URLs on unrelated hosts", () => { + expect( + collectValidUrls("https://evil.example/linkedin.com/in/person", []), + ).toEqual([]) + }) +}) diff --git a/apps/web/lib/url-helpers.ts b/apps/web/lib/url-helpers.ts index 0d3afa6ae..741f746a6 100644 --- a/apps/web/lib/url-helpers.ts +++ b/apps/web/lib/url-helpers.ts @@ -120,13 +120,40 @@ export const extractUrls = ( return { urls, duplicates } } +const parseWebUrl = (url: string): URL | null => { + const trimmed = url.trim() + if (!trimmed) return null + + try { + const parsed = new URL(trimmed) + return parsed.protocol === "http:" || parsed.protocol === "https:" + ? parsed + : null + } catch { + try { + return new URL(`https://${trimmed}`) + } catch { + return null + } + } +} + +const hostnameMatches = (hostname: string, domain: string): boolean => { + const normalizedHostname = hostname.toLowerCase() + return ( + normalizedHostname === domain || normalizedHostname.endsWith(`.${domain}`) + ) +} + /** * Checks if a URL is a Twitter/X URL. */ export const isTwitterUrl = (url: string): boolean => { - const normalizedUrl = url.toLowerCase() + const parsed = parseWebUrl(url) + if (!parsed) return false return ( - normalizedUrl.includes("twitter.com") || normalizedUrl.includes("x.com") + hostnameMatches(parsed.hostname, "twitter.com") || + hostnameMatches(parsed.hostname, "x.com") ) } @@ -134,11 +161,11 @@ export const isTwitterUrl = (url: string): boolean => { * Checks if a URL is a LinkedIn profile URL (not a company page). */ export const isLinkedInProfileUrl = (url: string): boolean => { - const normalizedUrl = url.toLowerCase() - return ( - normalizedUrl.includes("linkedin.com/in/") && - !normalizedUrl.includes("linkedin.com/company/") - ) + const parsed = parseWebUrl(url) + if (!parsed || !hostnameMatches(parsed.hostname, "linkedin.com")) return false + + const [section, handle] = parsed.pathname.split("/").filter(Boolean) + return section?.toLowerCase() === "in" && Boolean(handle) } /**