santifer/career-ops · error · Error

oraclecloud: untrusted hostname

Error message

oraclecloud: untrusted hostname "${parsed.hostname}" — must match *.fa[.<region>][.ocs].oraclecloud[1-99].com

What it means

assertOracleUrl validates a careers URL before any network request is made to Oracle Recruiting (Fusion) boards. The URL parsed fine and used HTTPS, but its hostname does not match the allowlist regex ORACLE_HOSTRE (/^[a-z0-9-]+\.fa\.(?:[a-z0-9-]+\.)?(?:ocs\.)?oraclecloud(?:[1-9][0-9]?)?\.com$/i), so the provider refuses to send a request to a host it does not recognize. This is an SSRF/trust guard, not a network failure — the request was never attempted.

Solutions

  1. Open portals.yml and fix the entry's careers_url to the real Fusion board host matching <tenant>.fa[.<region>][.ocs].oraclecloud[1-99].com (e.g. https://acme.fa.ocs.oraclecloud.com).
  2. Verify the hostname with the regex: node -e "console.log(/^[a-z0-9-]+\\.fa\\.(?:[a-z0-9-]+\\.)?(?:ocs\\.)?oraclecloud(?:[1-9][0-9]?)?\\.com$/i.test('acme.fa.ocs.oraclecloud.com'))".
  3. If Oracle genuinely serves this tenant on a new host shape, extend ORACLE_HOST_RE in providers/oraclecloud.mjs (line 50) and update the error message to match.
  4. Point the entry at the tenant's API base URL rather than a vanity domain; the provider derives API calls from the allowlisted host.

Example fix

// before (portals.yml)
careers_url: https://careers.acme.com/jobs
// after
careers_url: https://acme.fa.ocs.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1
Defensive patterns

Strategy: validation

Validate before calling

const ORACLE_HOST_RE = /^[a-z0-9-]+\.fa\.(?:[a-z0-9-]+\.)?(?:ocs\.)?oraclecloud(?:[1-9][0-9]?)?\.com$/i;
export function isOracleUrl(url) {
  try {
    const u = new URL(url);
    return u.protocol === 'https:' && ORACLE_HOST_RE.test(u.hostname);
  } catch { return false; }
}
if (!isOracleUrl(entry.careers_url)) throw new Error(`oraclecloud: untrusted or invalid careers_url for ${entry.name}`);

Type guard

function isOracleUrl(u) {
  if (typeof u !== 'string') return false;
  try {
    const parsed = new URL(u);
    return parsed.protocol === 'https:' &&
      /^[a-z0-9-]+\.fa\.(?:[a-z0-9-]+\.)?(?:ocs\.)?oraclecloud(?:[1-9][0-9]?)?\.com$/i.test(parsed.hostname);
  } catch { return false; }
}

Try / catch

try {
  await oracleProvider.fetch(entry, ctx);
} catch (e) {
  if (String(e.message).startsWith('oraclecloud: untrusted hostname')) {
    logger.warn({ entry: entry.name, url: entry.careers_url }, 'careers_url not on Oracle allowlist — check portals.yml');
    return null; // skip entry, keep scanning
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling providers/oraclecloud.mjs fetch (directly or via scan.mjs/verify-portals.mjs) with an entry whose careers_url points at a hostname outside the Oracle Fusion pattern, e.g. a custom vanity domain, a non-FA Oracle host (myhost.oraclecloud.com without .fa), a region misspelled, or a subdomain typo like acme.fa..oraclecloud.com.

Common situations: portals.yml misconfiguration: someone pasted the marketing careers page URL instead of the Fusion board URL; a company migrated to a vanity CNAME (careers.acme.com); the tenant uses a new regional host shape the regex doesn't yet allow; or a trailing/extra dot in the hostname.

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@e7abd431fc (2026-09-16). Data as JSON: /api/errors/680f9583ad9c09b1. Report an issue: GitHub.

Appendix: source

Thrown at providers/oraclecloud.mjs:70

const PAGE_SIZE = 200;
const MAX_PAGES = 25;             // safety cap (~5000 jobs); hard ceiling like workday
const RETRY_POLICY = { retries: 3 };
const INTER_PAGE_DELAY_MS = 250;  // WAF-aware spacing between same-host pages

// facetsList is a fixed constant on the finder; %3B is the encoded ';' separator.
const FACETS_LIST = 'LOCATIONS%3BWORK_LOCATIONS%3BWORKPLACE_TYPES%3BTITLES%3BCATEGORIES%3BORGANIZATIONS%3BPOSTING_DATES%3BFLEX_FIELDS';

/** @param {string} url */
function assertOracleUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`oraclecloud: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`oraclecloud: URL must use HTTPS: ${url}`);
  if (!ORACLE_HOST_RE.test(parsed.hostname)) {
    throw new Error(`oraclecloud: untrusted hostname "${parsed.hostname}" — must match *.fa[.<region>][.ocs].oraclecloud[1-99].com`);
  }
  return url;
}

// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.
// (copied from greenhouse.mjs)
function toEpochMs(value) {
  if (!value) return undefined;
  const parsed = Date.parse(value);
  return Number.isNaN(parsed) ? undefined : parsed;
}

function sleep(ms, ctx) {
  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**

View on GitHub (pinned to e7abd431fc)