santifer/career-ops · error
comeet: untrusted hostname
Error message
comeet: untrusted hostname "${parsed.hostname}" — must be ${COMEET_API_HOST} What it means
As an SSRF defense, the Comeet provider pins the API hostname to the single fixed origin www.comeet.co (combined with redirect:'error' so a redirect can't move the request off-host). A URL that parses and is https but points at any other hostname is rejected with this error naming the offending host. This prevents a crafted portal entry from making the scanner call an arbitrary internal service.
Solutions
- Rewrite the entry's api to the canonical origin: https://www.comeet.co/careers-api/2.0/company/<uid>/positions?token=<token>
- Find the company-uid and token from the tenant's actual careers-api link (inspect the network tab on their careers page) — Comeet has no slug→API shortcut
- Check for lookalike/typo hostnames (www.comeet.com, comeet.co without www) and correct them
- If you legitimately need a different host (self-hosted proxy), you must modify COMEET_API_HOST in providers/comeet.mjs — don't try to sneak it via the entry config
Example fix
// before api: https://acme.comeet.co/careers-api/2.0/company/acme/positions?token=abc // after api: https://www.comeet.co/careers-api/2.0/company/acme/positions?token=abc
Defensive patterns
Strategy: validation
Validate before calling
const COMEET_API_HOST = 'www.comeet.co';
function isCanonicalComeetOrigin(raw) {
try {
const u = new URL(raw);
return u.protocol === 'https:' && u.hostname === COMEET_API_HOST && u.pathname.startsWith('/careers-api/');
} catch { return false; }
}
if (!isCanonicalComeetOrigin(entry.api)) throw new Error(`entry ${entry.name}: comeet api must be https://www.comeet.co/careers-api/...`); Type guard
function isComeetApiUrl(raw) {
if (typeof raw !== 'string' || !raw) return false;
try {
const u = new URL(raw);
return u.protocol === 'https:' && u.hostname === 'www.comeet.co' && u.pathname.startsWith('/careers-api/');
} catch { return false; }
} Try / catch
try {
assertComeetUrl(entry.api);
} catch (err) {
if (String(err.message).includes('untrusted hostname')) {
logger.error({entry: entry.name}, 'comeet api must point at https://www.comeet.co — rebuild the URL with the tenant uid and token');
} else throw err;
} Prevention
- Remember Comeet has no slug shortcut: always supply the full careers-api URL with company uid + token from the tenant's live careers page
- Never use branded tenant domains (acme.comeet.co) — only the fixed www.comeet.co origin is allowed
- Beware lookalike hostnames (www.comeet.co.evil.io); the exact-hostname check is intentional SSRF protection
- Keep redirect:'error' semantics in mind: the pinned host must be the first hop, not just the final one
- Review any portal entry whose URL you didn't construct yourself before adding it to shared config
When it happens
Trigger: fetch() gets an entry whose api/careers_url is https and URL-parseable but whose hostname is not exactly 'www.comeet.co' — e.g. comeet.com (no www), a spoofed subdomain (www.comeet.co.evil.io), a company's own careers domain, or an IP address.
Common situations: Using the tenant's branded careers URL (acme.comeet.co or acme.com/jobs) instead of the fixed www.comeet.co careers-api origin; a typo like ww.comeet.co; hostile/misconfigured portal entries in a shared config; assuming Comeet works like Greenhouse where per-tenant subdomains (boards.greenhouse.io/<slug>) are the norm.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- breezy: untrusted hostname
- builtin: untrusted hostname
- careerviet: untrusted hostname
- arbeitnow: untrusted hostname
- ashby: untrusted hostname
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/2881fbc8e24e2687.
Report an issue: GitHub.
Appendix: source
Thrown at providers/comeet.mjs:38
try {
parsed = new URL(raw);
} catch {
return false;
}
return parsed.protocol === 'https:' && parsed.hostname === COMEET_API_HOST && parsed.pathname.startsWith('/careers-api/');
}
/** @param {string} url */
function assertComeetUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`comeet: invalid URL: ${redactToken(url)}`);
}
if (parsed.protocol !== 'https:') throw new Error(`comeet: URL must use HTTPS: ${redactToken(url)}`);
if (parsed.hostname !== COMEET_API_HOST)
throw new Error(`comeet: untrusted hostname "${parsed.hostname}" — must be ${COMEET_API_HOST}`);
if (!parsed.pathname.startsWith('/careers-api/'))
throw new Error(`comeet: URL path must be the careers-api endpoint: ${redactToken(url)}`);
return url;
}
// Redact the per-tenant ?token= so neither the (informational, possibly-logged)
// DetectHit url nor a thrown validation error carries the secret. Best-effort:
// falls back to a regex strip when the value can't be parsed as a URL.
function redactToken(url) {
try {
const parsed = new URL(url);
if (parsed.searchParams.has('token')) parsed.searchParams.set('token', 'REDACTED');
return parsed.href;
} catch {
return typeof url === 'string' ? url.replace(/([?&]token=)[^&#]*/gi, '$1REDACTED') : url;
}
}
View on GitHub (pinned to aac998c7ed)