santifer/career-ops · error · Error

pinpoint: untrusted hostname

Error message

pinpoint: untrusted hostname "${parsed.hostname}" — must match <slug>.pinpointhq.com

What it means

assertPinpointUrl only accepts hostnames matching PINPOINT_HOST_RE — <slug>.pinpointhq.com. The URL parsed and used HTTPS, but the hostname is outside that allowlist, so the provider refuses to send the request. This is the SSRF/trust guard: the hostname could be an attacker-controlled or simply wrong domain.

Solutions

  1. Replace careers_url in portals.yml with the tenant's real https://<slug>.pinpointhq.com host (find the slug from the company's careers page or Pinpoint feed link).
  2. If Pinpoint serves this tenant on a new host shape, update PINPOINT_HOST_RE at pinpoint.mjs line 22 and align the error message.
  3. Resolve the vanity domain once (follow the redirect manually) and hardcode the underlying pinpointhq.com tenant host.
  4. Do not bypass by enabling redirects in fetchText — the redirect:'error' flag plus this assertion is the SSRF guarantee.

Example fix

// before (portals.yml)
careers_url: https://jobs.acme.com
// after
careers_url: https://acme.pinpointhq.com
Defensive patterns

Strategy: validation

Validate before calling

const PINPOINT_HOST_RE = /^[a-z0-9-]+\.pinpointhq\.com$/;
export function isPinpointUrl(u) {
  try {
    const parsed = new URL(u);
    return parsed.protocol === 'https:' && PINPOINT_HOST_RE.test(parsed.hostname);
  } catch { return false; }
}
if (!isPinpointUrl(entry.careers_url)) throw new Error(`pinpoint: careers_url for ${entry.name} not on Pinpoint allowlist`);

Type guard

function isPinpointTenantUrl(u) {
  if (typeof u !== 'string') return false;
  try {
    const parsed = new URL(u);
    return parsed.protocol === 'https:' && /^[a-z0-9-]+\.pinpointhq\.com$/.test(parsed.hostname);
  } catch { return false; }
}

Try / catch

try {
  await pinpointProvider.fetch(entry, ctx);
} catch (e) {
  if (String(e.message).startsWith('pinpoint: untrusted hostname')) {
    logger.warn({ entry: entry.name, host: (() => { try { return new URL(entry.careers_url).hostname; } catch { return '?'; } })() }, 'not a *.pinpointhq.com tenant — use the tenant subdomain, not a vanity domain');
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling fetch or validation paths reaching assertPinpointUrl (pinpoint.mjs line 34) with a careers_url host like acme.pinpointhq.co (wrong TLD), jobs.acme.com (vanity domain), pinpointhq.com.evil.test, or an extra subdomain level.

Common situations: Company uses a vanity domain fronting Pinpoint instead of the tenant subdomain; TLD typo (.co vs .com); an entry actually belonging to a different ATS; malicious/mistaken edit of portals.yml pointing off-domain.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/280485972fd101e4. Report an issue: GitHub.

Appendix: source

Thrown at providers/pinpoint.mjs:34

// The tenant label must be a valid DNS label: it may contain hyphens but must
// not start or end with one (so `acme-.pinpointhq.com` is rejected). The
// optional trailing group keeps single-character labels (e.g. `a.pinpointhq.com`)
// valid. detect() and fetch() both route through this constant via
// resolveApiUrl()/assertPinpointUrl(), so the stricter check applies everywhere.
const PINPOINT_HOST_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.pinpointhq\.com$/;

/** @param {string} url */
function assertPinpointUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`pinpoint: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`pinpoint: URL must use HTTPS: ${url}`);
  if (!PINPOINT_HOST_RE.test(parsed.hostname)) {
    throw new Error(`pinpoint: untrusted hostname "${parsed.hostname}" — must match <slug>.pinpointhq.com`);
  }
  return url;
}

function resolveApiUrl(entry) {
  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
  if (!raw) return null;
  let parsed;
  try {
    parsed = new URL(raw);
  } catch {
    return null;
  }
  if (parsed.protocol !== 'https:') return null;
  if (!PINPOINT_HOST_RE.test(parsed.hostname)) return null;
  return `https://${parsed.hostname}/postings.json`;
}

View on GitHub (pinned to aac998c7ed)