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
- 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.
- 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).
- Fix TLD typos: `ashbyhq.com`/`ashby.co` are the real domains, not `ashby.com`.
- 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
- Keep the allowlist of Ashby hosts visible in config docs and check entries against it.
- Point corporate careers pages at a display field; reserve `api:` for the genuine Ashby endpoint.
- Double-check TLDs: ashby.co and ashbyhq.com, not ashby.com.
- Treat hostname allowlist failures as config bugs, not runtime retries.
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
- arbeitnow: untrusted hostname
- bamboohr: untrusted hostname
- breezy: untrusted hostname
- builtin: untrusted hostname
- careerviet: untrusted hostname
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)