santifer/career-ops · error

ashby: untrusted hostname

Error message

ashby: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_ASHBY_HOSTS].join(', ')}

What it means

The final gate in assertAshbyUrl: even an HTTPS URL whose hostname is not in the ALLOWED_ASHBY_HOSTS set is rejected. This is an SSRF/anti-spoofing guard — the provider only ever talks to genuine Ashby board hosts, so a config entry pointing at an arbitrary domain can never be used to make the scanner fetch attacker-controlled endpoints.

Solutions

  1. Use the real Ashby host for the board — typically `https://jobs.ashby.co/<org>` or the posting API on `jobs.ashbyhq.com`; check the exact allowed hosts in the error message.
  2. If the entry has a corporate careers URL, move it to the entry's human-facing field and set the explicit `api:` field to the Ashby API URL (resolveApiUrl honors an explicit api).
  3. Fix TLD typos: `ashbyhq.com`/`ashby.co` are the real domains, not `ashby.com`.
  4. Only if you maintain the provider: add the verified host to ALLOWED_ASHBY_HOSTS after confirming it genuinely is Ashby-hosted.

Example fix

// before
assertAshbyUrl('https://careers.exampleco.com/listing'); // untrusted hostname

// after
assertAshbyUrl('https://jobs.ashby.co/exampleco'); // allowed host
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED = new Set(['jobs.ashby.co', 'jobs.ashbyhq.com', 'api.ashbyhq.com']);
function isAllowedAshbyHost(url) {
  try { return ALLOWED.has(new URL(url).hostname); } catch { return false; }
}

Type guard

function isAshbyEntry(entry) {
  return typeof entry?.api === 'string' && isAllowedAshbyHost(entry.api);
}

Try / catch

try {
  assertAshbyUrl(apiUrl);
} catch (err) {
  if (/untrusted hostname/.test(err.message)) {
    console.warn(`Entry points at a non-Ashby host; use the real Ashby board URL. ${err.message}`);
    return null;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling assertAshbyUrl with e.g. `https://careers.example.com/api/ashby` or `https://ashby.example.net/...` — hosts that look Ashby-related but are not in ALLOWED_ASHBY_HOSTS (e.g. `jobs.ashby.co`, `api.ashbyhq.com`).

Common situations: Config entries pointing at a company's own careers site instead of its Ashby-hosted board, typos like `jobs.ashby.com` (wrong TLD), or custom CNAME domains served by Ashby that the allowlist does not cover.

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

Appendix: source

Thrown at providers/ashby.mjs:93

    min: Math.min(resolvedMin, resolvedMax),
    max: Math.max(resolvedMin, resolvedMax),
    currency: currency.toUpperCase(),
  };
}

const ALLOWED_ASHBY_HOSTS = new Set(['api.ashbyhq.com']);

/** @param {string} url */
function assertAshbyUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`ashby: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`ashby: URL must use HTTPS: ${url}`);
  if (!ALLOWED_ASHBY_HOSTS.has(parsed.hostname))
    throw new Error(`ashby: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_ASHBY_HOSTS].join(', ')}`);
  return url;
}

/** @param {import('./_types.js').PortalEntry} entry */
function resolveApiUrl(entry) {
  // Explicit api: wins — lets an entry keep a human-facing corporate
  // careers_url (e.g. https://openai.com/careers) while still pinning the
  // Ashby posting-api board (mirrors greenhouse's api: precedence).
  if (entry.api) {
    assertAshbyUrl(entry.api);
    return entry.api;
  }
  const url = entry.careers_url || '';
  const match = url.match(/jobs\.ashbyhq\.com\/([^/?#]+)/);
  if (!match) return null;
  return `https://api.ashbyhq.com/posting-api/job-board/${match[1]}?includeCompensation=true`;
}

View on GitHub (pinned to aac998c7ed)