santifer/career-ops · error

eightfold: untrusted hostname

Error message

eightfold: untrusted hostname "${parsed.hostname}" — must match *.eightfold.ai

What it means

assertEightfoldUrl is the provider's SSRF guard: every request URL must parse, use HTTPS, and have a hostname matching /^[a-z0-9-]+\.eightfold\.ai$/i before it is fetched. This error is thrown when a URL passes parsing and the HTTPS check but its hostname is not an *.eightfold.ai tenant host — e.g. a branded CNAME like careers.<company>.com or a hand-edited host. Eightfold's jobs API is host-pinned to *.eightfold.ai, so non-canonical hosts are refused rather than fetched.

Solutions

  1. Change the portal entry's careers_url (or set entry.api) to the canonical https://<tenant>.eightfold.ai form; branded CNAMEs are deliberately not accepted.
  2. Verify the hostname matches a single-label subdomain of eightfold.ai — no multi-level subdomains (a.b.eightfold.ai fails the regex) and no paths/ports; the API is inferred from the tenant host alone.
  3. If you need to keep a branded careers page for display, keep it in careers_url and pin the tenant host via entry.api, which takes precedence.
  4. If this fires unexpectedly in your own code, log parsed.hostname from the thrown message and diff it against EIGHTFOLD_HOST_RE before retrying.

Example fix

// before (portals.yml entry)
- name: Bayer
  careers_url: https://talent.bayer.com/careers
// after
- name: Bayer
  careers_url: https://bayer.eightfold.ai/careers
Defensive patterns

Strategy: validation

Validate before calling

function isEightfoldUrl(url) {
  try {
    const u = new URL(url);
    return u.protocol === 'https:' && /^[a-z0-9-]+\.eightfold\.ai$/i.test(u.hostname);
  } catch { return false; }
}
// before calling: if (!isEightfoldUrl(entry.careers_url)) throw ...

Type guard

const isTrustedEightfoldHost = (hostname) => typeof hostname === 'string' && /^[a-z0-9-]+\.eightfold\.ai$/i.test(hostname);

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).startsWith('eightfold: untrusted hostname')) {
    console.error(`Config error: ${entry.name} must use an *.eightfold.ai host, got: ${entry.careers_url}`);
    return [];
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling provider.fetch() (which calls assertEightfoldUrl before every page request) with a portal entry whose api/careers_url points at a non-eightfold.ai host — practically this means the built-in apiUrl was tampered with, a custom caller passes its own URL into an exported path, or entry data was mutated between resolveTenant and fetch. resolveTenant itself rejects off-host entries, so the realistic trigger is feeding this guard a URL directly or config drift.

Common situations: Configuring a portal entry with the company's branded careers CNAME (talent.bayer.com) instead of the canonical bayer.eightfold.ai; copying a jobs URL that includes a path on a different subdomain; typos like .eightfold.com lookalike hosts; tests asserting the guard fires for hostile hostnames (SSRF probing).

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/c4e525f55b05f12c. Report an issue: GitHub.

Appendix: source

Thrown at providers/eightfold.mjs:73

const RETRY_POLICY = { retries: 3, baseDelayMs: 500, maxDelayMs: 8_000 };

/**
 * SSRF guard — every request URL passes through here before it is fetched.
 *
 * @param {string} url
 * @returns {string} the same URL, when it is a trusted Eightfold endpoint.
 */
function assertEightfoldUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`eightfold: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`eightfold: URL must use HTTPS: ${url}`);
  if (!EIGHTFOLD_HOST_RE.test(parsed.hostname)) {
    throw new Error(`eightfold: untrusted hostname "${parsed.hostname}" — must match *.eightfold.ai`);
  }
  return url;
}

/** @param {number} ms @param {any} ctx */
function sleep(ms, ctx) {
  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Eightfold reports timestamps as epoch SECONDS (`t_create`, `t_update`), not
 * the ISO strings every other provider gets. Converted here; anything
 * non-finite or non-positive is dropped rather than guessed at.
 *
 * @param {unknown} value
 * @returns {number|undefined} epoch ms, or undefined.
 */

View on GitHub (pinned to aac998c7ed)