santifer/career-ops · error
gem: untrusted hostname
Error message
gem: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_GEM_HOSTS].join(', ')} What it means
assertGemUrl enforces a hostname allowlist (ALLOWED_GEM_HOSTS) as an SSRF guard: only Gem's official board/API hosts may be fetched. A URL that parses and is HTTPS but whose hostname is not in the allowlist is rejected with this message listing the permitted hosts.
Solutions
- Read the allowed hosts from the error message and correct the hostname to one of them.
- If the host is legitimately Gem and genuinely new, update ALLOWED_GEM_HOSTS in providers/gem.mjs after verifying the domain (a deliberate code change, not a config tweak).
- Double-check for typos — the allowlist match is exact, no subdomain wildcards.
- If the URL is from an untrusted source, do not add it to the allowlist; the rejection is the guard working.
Example fix
// before "api": "https://job-boards.gem.co.evil.com/v1/boards/acme" // after "api": "https://job-boards.gem.co/v1/boards/acme"
Defensive patterns
Strategy: validation
Validate before calling
import { ALLOWED_GEM_HOSTS } from './providers/gem.mjs';
const host = new URL(u).hostname;
if (!ALLOWED_GEM_HOSTS.has(host)) throw new Error(`Host ${host} not in Gem allowlist`); Type guard
const isAllowedHost = (u) => { try { return ALLOWED_GEM_HOSTS.has(new URL(u).hostname); } catch { return false; } }; Try / catch
try { assertGemUrl(url); } catch (e) { if (e.message.includes('untrusted hostname')) console.error('SSRF guard tripped — verify this domain really belongs to Gem before allowlisting'); throw e; } Prevention
- Only add hosts to ALLOWED_GEM_HOSTS after verifying domain ownership
- Never accept board URLs from untrusted input (postings, emails) without this check
- Treat allowlist rejections as a security signal, not an inconvenience
- Keep the allowlist minimal — exact hosts, no wildcards
When it happens
Trigger: Configuring a board with api: https://evil.example.com/... or a mirror/proxy host not in ALLOWED_GEM_HOSTS; a typo'd hostname (boards.gem.co instead of the allowed one); a vendor-supplied URL pointing at a custom domain.
Common situations: Adding a board whose URL lives on a newly introduced Gem domain added after this library's allowlist was written; typosquat or config-injection attempts (which the check exists to block); copy-pasting a third-party aggregator URL.
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
- ashby: untrusted hostname
- bamboohr: untrusted hostname
- breezy: untrusted hostname
- builtin: untrusted hostname
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/0ada6427eba2b0f1.
Report an issue: GitHub.
Appendix: source
Thrown at providers/gem.mjs:127
const body = htmlToText(posting?.descriptionHtml);
const outro = htmlToText(posting?.jobPostSectionHtml?.outroHtml);
const compensation = htmlToText(posting?.compensationHtml);
const text = [intro, body, outro].filter(Boolean).join('\n\n');
return compensation ? [text, `Compensation: ${compensation}`].filter(Boolean).join('\n\n') : text;
}
/** @param {string} url */
function assertGemUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`gem: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`gem: URL must use HTTPS: ${url}`);
if (!ALLOWED_GEM_HOSTS.has(parsed.hostname))
throw new Error(`gem: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_GEM_HOSTS].join(', ')}`);
return url;
}
/** Resolve an explicitly pinned URL for Gem's documented REST job-board API. */
function resolveRestApiUrl(entry) {
const raw = typeof entry.api === 'string' ? entry.api : '';
if (!raw) return null;
let parsed;
try {
parsed = new URL(raw);
} catch {
return null;
}
if (parsed.protocol !== 'https:' || parsed.hostname !== 'api.gem.com') return null;
if (!/^\/job_board\/v0\/[^/?#]+\/job_posts\/?$/.test(parsed.pathname)) return null;
return parsed;
}
View on GitHub (pinned to aac998c7ed)