Ctrl K
ComunidadGuías…

lib/linkedin-url.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * linkedin-url.mjs — read a LinkedIn job-posting URL.
 *
 * The `linkedin` rung of liveness-api.mjs kept this parser inside its
 * ATS_PROVIDERS entry, where nothing else could import it, and url-key.mjs
 * carried a second copy for aggregatorPostingId. It lives here so there is one
 * body to import and to test parity against.
 *
 * No imports and no Node-only APIs (only the global `URL`), so a browser bundle
 * can load it as well as the CLI.
 */

/**
 * The numeric LinkedIn job id carried by this URL, or null when it is not one
 * posting. Matched as digits only, so nothing user-supplied is ever spliced
 * into a hostname.
 *
 * The host test is anchored at a label boundary, so regional subdomains
 * (uk.linkedin.com) match and "linkedin.com.evil.example" never does.
 * `u.hostname` is already lowercased by the URL parser. The protocol is not
 * checked here: callers decide that (resolveAtsApi refuses anything but https).
 *
 * @param {URL} u
 * @returns {string|null}
 */
export function linkedInJobId(u) {
  if (!/(^|\.)linkedin\.com$/.test(u.hostname)) return null;
  return linkedInPathJobId(u);
}

/**
 * The job id in the path or query of a URL already known to be on LinkedIn,
 * without checking the host. For callers that matched the host their own way:
 * url-key.mjs folds trailing dots before it decides a URL is on linkedin.com.
 *
 * Four recognized shapes:
 *   /jobs/view/{id}
 *   /jobs/view/{title-slug}-{id}
 *   /comm/jobs/view/{id}, the link in every LinkedIn email. Verified against
 *     linkedin.com itself: `/comm/jobs/view/{id}?trk=…` answers 307 with
 *     `session_redirect=/jobs/view/{id}?trk=…`, the same id with `/comm` dropped.
 *   any page carrying the posting in ?currentJobId= (search and collection views)
 *
 * @param {URL} u
 * @returns {string|null}
 */
export function linkedInPathJobId(u) {
  const path = u.pathname.match(/^\/(?:jobs\/view\/(?:.*-)?|comm\/jobs\/view\/)(\d+)\/?$/);
  if (path) return path[1];
  const current = u.searchParams.get('currentJobId');
  return current && /^\d+$/.test(current) ? current : null;
}

function foldLinkedInHost(host) {
  const h = String(host).toLowerCase();
  return h.length > 1 && h.endsWith('.') && !h.endsWith('..') ? h.slice(0, -1) : h;
}

export function linkedInCanonicalKey(raw) {
  if (typeof raw !== 'string') return '';
  let u;
  try { u = new URL(raw.trim()); } catch { return ''; }
  if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
  const host = foldLinkedInHost(u.hostname);
  if (!/(^|\.)linkedin\.com$/.test(host)) return '';
  const id = linkedInPathJobId(u);
  return id ? `linkedin.com:${id}` : '';
}

/**
 * The public guest endpoint for a posting id: the rendered posting as HTML, with
 * no auth and no browser. Pass only an id returned by `linkedInJobId`, so the
 * host stays a fixed literal and nothing user-supplied reaches it.
 *
 * @param {string} id
 * @returns {string}
 */
export function linkedInGuestUrl(id) {
  return `https://www.linkedin.com/jobs-guest/jobs/api/jobPosting/${id}`;
}