santifer/career-ops · error · Error
flowxtra: untrusted hostname "${parsed.hostname}" — must be
Error message
flowxtra: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_ENDPOINT_HOST} What it means
flowxtra.mjs throws this inside assertFlowxtraEndpointUrl(), a host-allowlist guard run before every ctx.fetchJson against the Flowxtra central jobs API. It compares parsed.hostname to the compile-time constant TRUSTED_ENDPOINT_HOST ('app.flowxtra.com') and aborts if they differ, so the request never leaves the process. The guard is an SSRF defense: combined with redirect:'error' it guarantees the only host ever contacted is app.flowxtra.com.
Source
Thrown at providers/flowxtra.mjs:35
const JOBS_ENDPOINT = 'https://app.flowxtra.com/api/central/jobs';
const TRUSTED_ENDPOINT_HOST = 'app.flowxtra.com';
const TRUSTED_APPLY_HOST = 'flowxtra.com';
const PER_PAGE = 100;
const DEFAULT_MAX_PAGES = 3;
const MAX_PAGES_CAP = 50;
/** @param {string} url */
function assertFlowxtraEndpointUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`flowxtra: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`flowxtra: URL must use HTTPS: ${url}`);
if (parsed.hostname !== TRUSTED_ENDPOINT_HOST) {
throw new Error(`flowxtra: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_ENDPOINT_HOST}`);
}
return url;
}
/** Resolve the page cap: a positive integer `max_pages` on the entry, capped. */
function resolveMaxPages(entry) {
const v = entry?.max_pages;
if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES_CAP);
return DEFAULT_MAX_PAGES;
}
// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.
function toEpochMs(value) {
if (!value) return undefined;
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? undefined : parsed;
}
View on GitHub (pinned to 9b17a8ac97)
Solutions
- If you intended to point at app.flowxtra.com, leave JOBS_ENDPOINT and TRUSTED_ENDPOINT_HOST at their shipped values and remove any local override.
- If you genuinely need a different host (mirror/proxy), update BOTH JOBS_ENDPOINT and TRUSTED_ENDPOINT_HOST to the same hostname in the same edit.
- If this surfaced in a test, pass a URL whose hostname is app.flowxtra.com (e.g. https://app.flowxtra.com/api/central/jobs?page=1) rather than example.com.
- Search the repo for any code path that calls assertFlowxtraEndpointUrl() with a non-constant argument and make it derive from JOBS_ENDPOINT instead.
Example fix
// before const JOBS_ENDPOINT = 'https://staging.flowxtra.com/api/central/jobs'; const TRUSTED_ENDPOINT_HOST = 'app.flowxtra.com'; // mismatch -> throws // after const JOBS_ENDPOINT = 'https://app.flowxtra.com/api/central/jobs'; const TRUSTED_ENDPOINT_HOST = 'app.flowxtra.com';
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight: assert the Flowxtra endpoint constant is intact before scanning.
function checkFlowxtraEndpoint() {
const u = new URL('https://app.flowxtra.com/api/central/jobs'); // expected literal
if (u.hostname !== 'app.flowxtra.com') throw new Error('flowxtra endpoint host drifted');
if (u.protocol !== 'https:') throw new Error('flowxtra endpoint not HTTPS');
}
// Run once at startup; abort the scan if it throws. Try / catch
// At the scan-runner boundary: isolate one provider's failure so the whole scan continues.
try {
const jobs = await flowxtraProvider.fetch(entry, ctx);
} catch (err) {
if (/flowxtra: untrusted hostname/.test(err.message)) {
console.error(`Skipping ${entry.name}: ${err.message} (constant drift — fix in flowxtra.mjs)`);
return [];
}
throw err;
} Prevention
- Treat JOBS_ENDPOINT and TRUSTED_ENDPOINT_HOST as a paired constant — change them together or not at all.
- Never call assertFlowxtraEndpointUrl() with a computed/foreign URL in tests; derive test inputs from JOBS_ENDPOINT.
- Add a unit test that asserts new URL(JOBS_ENDPOINT).hostname === TRUSTED_ENDPOINT_HOST so drift fails CI.
When it happens
Trigger: Fires only when the URL passed to assertFlowxtraEndpointUrl() resolves to a hostname other than app.flowxtra.com. In production that URL is built from the module constant JOBS_ENDPOINT ('https://app.flowxtra.com/api/central/jobs'), so a live throw means someone edited JOBS_ENDPOINT/TRUSTED_ENDPOINT_HOST to disagree, a test injected a foreign URL, or the constant was accidentally set to an http/staging host.
Common situations: A contributor redirects the provider at a mirror/staging host but changes only JOBS_ENDPOINT and forgets TRUSTED_ENDPOINT_HOST; a unit test calls assertFlowxtraEndpointUrl() directly with a throwaway URL; an env-injection or config-merge accidentally rewrites the constant to a different domain.
Related errors
- gem: untrusted hostname "${parsed.hostname}" — must be one o
- getonbrd: untrusted hostname "${parsed.hostname}" — must be
- glints: untrusted hostname "${parsed.hostname}" — must be on
- greenhouse: untrusted hostname "${parsed.hostname}" — must b
- gem: invalid URL: ${url}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/3a8ab29ede5dbcf7.
Report an issue: GitHub.