santifer/career-ops · error · Error

recruitee: untrusted hostname "${parsed.hostname}" — must ma

Error message

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

What it means

assertRecruiteeUrl throws when parsed.hostname fails RECRUITEE_HOST_RE (/^[a-z0-9][a-z0-9-]*\.recruitee\.com$/). The hostname must be exactly <slug>.recruitee.com — slug starts alphanumeric, then any mix of lowercase alphanumerics and hyphens. This SSRF guard prevents a crafted entry from pointing the fetch at an arbitrary host while still allowing the per-tenant slug to vary.

Source

Thrown at providers/recruitee.mjs:21

// Recruitee provider — hits the public per-tenant offers API.
// Auto-detects from careers_url pattern `https://<slug>.recruitee.com`.
// Per-tenant subdomains are the variable part — SSRF defence uses a
// regex match on `<safe-slug>.recruitee.com` rather than a static
// allowlist.

const RECRUITEE_HOST_RE = /^[a-z0-9][a-z0-9-]*\.recruitee\.com$/;

function assertRecruiteeUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`recruitee: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`recruitee: URL must use HTTPS: ${url}`);
  if (!RECRUITEE_HOST_RE.test(parsed.hostname)) {
    throw new Error(`recruitee: untrusted hostname "${parsed.hostname}" — must match <slug>.recruitee.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 (!RECRUITEE_HOST_RE.test(parsed.hostname)) return null;
  return `https://${parsed.hostname}/api/offers/`;
}

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Use the canonical https://<slug>.recruitee.com subdomain where <slug> is lowercase alphanumeric plus hyphens.
  2. If the board only has a branded domain, the recruitee provider cannot auto-derive it — supply api: pointing to the recruitee.com subdomain or use the correct provider.
  3. Lowercase the hostname and confirm it is a single-label subdomain of recruitee.com.
  4. Check for typos in the TLD (.org, .net vs .com).

Example fix

// before
{ careers_url: 'https://jobs.acme.com' }
// after
{ careers_url: 'https://acme.recruitee.com' }
Defensive patterns

Strategy: validation

Validate before calling

const RECRUITEE_HOST_RE = /^[a-z0-9][a-z0-9-]*\.recruitee\.com$/;
function isRecruiteeUrl(url) {
  try {
    const p = new URL(url);
    return p.protocol === 'https:' && RECRUITEE_HOST_RE.test(p.hostname);
  } catch { return false; }
}
if (!isRecruiteeUrl(entry.careers_url)) {
  console.warn(`skip ${entry.name}: not a *.recruitee.com URL`);
}

Type guard

null

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (e) {
  if (/untrusted hostname/.test(e.message)) {
    console.warn(`[skip] ${entry.name}: ${e.message}`);
  } else throw e;
}

Prevention

When it happens

Trigger: A hostname like jobs.acme.com (branded, not recruitee.com); acme.RECRUITEE.com (uppercase); acme.recruitee.org (wrong TLD); recruitee.com with no slug; a multi-level subdomain like a.b.recruitee.com (the regex requires exactly one label before .recruitee.com).

Common situations: A tenant uses a Recruitee vanity/branded domain that drops the recruitee.com suffix; an entry was misclassified as recruitee when it belongs to a different provider; the slug was typed with an uppercase letter.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/94a4625f7a9fa49d. Report an issue: GitHub.