import type { Page } from "playwright"; /** * Visits a LinkedIn profile page. This registers as a profile view on LinkedIn. * Navigates and waits, then reports whether the page shows a 1st-degree badge — * lets the runner backfill degree=1 for contacts that were already connected * before Linki ever sent them a connection request (e.g. manually added leads). * * Primary-degree signal: presence of the profile's primary "Message" link * (a[href*="/messaging/compose"]) — only shown to 1st-degree connections, reads * an href attribute rather than a CSS class or translated text, so it survives * both LinkedIn's periodic class-name hashing and non-English UI languages. * * MUST be scoped to the visited person's own intro/top card — a page-wide * search also matches "Message" links belonging to OTHER people rendered * elsewhere on the page (sidebar modules like "People also viewed" / suggested * connections). For a non-connected target, THEIR page has no such link, but * the sidebar still does — a page-wide `.first()` silently grabbed a random * unrelated 1st-degree connection's link, wrongly marked the target degree=1, * and stored that stranger's messaging URN, causing messages to go to the * wrong person entirely (incident: Jul 2026, see CLAUDE.md/memory). The top * card is identified structurally as the section containing the page's own *

name heading, which is robust to LinkedIn's class-name hashing. * * Falls back to the old text-scrape (/\b1st\b/ within that same top card) * when the link isn't found, in case the current account's profile layout * doesn't render it as a plain link (e.g. buried behind a click/menu). * * The same link's href carries the messaging URN (urn:li:fsd_profile:ACoAA...) * needed to message this person directly later without a name-search typeahead * — see lib/linkedin/message.ts. Returned as messagingUrn when found. */ export async function visitProfile(page: Page, linkedinUrl: string): Promise<{ isFirstDegree: boolean; messagingUrn: string | null }> { await page.goto(linkedinUrl, { waitUntil: "domcontentloaded", timeout: 30000 }); await page.waitForTimeout(3000 + Math.random() * 2000); const topCard = page.locator("main section").filter({ has: page.locator("h1") }).first(); const messageLink = topCard.locator('a[href*="/messaging/compose"]').first(); const messageHref = (await messageLink.count()) > 0 ? await messageLink.getAttribute("href").catch(() => null) : null; const urnMatch = messageHref?.match(/profileUrn=([^&]+)/); const messagingUrn = urnMatch ? decodeURIComponent(urnMatch[1]) : null; if (messageHref) return { isFirstDegree: true, messagingUrn }; const pageText = await topCard.innerText().catch(() => ""); return { isFirstDegree: /\b1st\b/.test(pageText), messagingUrn: null }; }