{"record":{"id":"dd028b5327cb46e7","repo":"santifer/career-ops","slug":"oraclecloud-invalid-url-url","errorCode":null,"errorMessage":"oraclecloud: invalid URL: ${url}","messagePattern":"oraclecloud: invalid URL: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/oraclecloud.mjs","lineNumber":66,"sourceCode":"// oraclecloud99.com. No leading zero, at most two digits — a bounded family,\n// so this stays a host pin and never becomes a wildcard apex match.\nconst ORACLE_HOST_RE = /^[a-z0-9-]+\\.fa\\.(?:[a-z0-9-]+\\.)?(?:ocs\\.)?oraclecloud(?:[1-9][0-9]?)?\\.com$/i;\n\nconst PAGE_SIZE = 200;\nconst MAX_PAGES = 25;             // safety cap (~5000 jobs); hard ceiling like workday\nconst RETRY_POLICY = { retries: 3 };\nconst INTER_PAGE_DELAY_MS = 150;  // WAF-aware spacing between same-host pages\n\n// facetsList is a fixed constant on the finder; %3B is the encoded ';' separator.\nconst FACETS_LIST = 'LOCATIONS%3BWORK_LOCATIONS%3BWORKPLACE_TYPES%3BTITLES%3BCATEGORIES%3BORGANIZATIONS%3BPOSTING_DATES%3BFLEX_FIELDS';\n\n/** @param {string} url */\nfunction assertOracleUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`oraclecloud: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`oraclecloud: URL must use HTTPS: ${url}`);\n  if (!ORACLE_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`oraclecloud: untrusted hostname \"${parsed.hostname}\" — must match *.fa[.<region>][.ocs].oraclecloud[1-99].com`);\n  }\n  return url;\n}\n\n// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.\n// (copied from greenhouse.mjs)\nfunction toEpochMs(value) {\n  if (!value) return undefined;\n  const parsed = Date.parse(value);\n  return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nfunction sleep(ms, ctx) {\n  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/oraclecloud.mjs#L48-L84","documentation":"Thrown by oraclecloud's assertOracleUrl() when new URL(url) raises — the URL string is syntactically unparseable. This is the first of three SSRF gates (valid URL → HTTPS → trusted hostname regex) that pin Oracle Cloud (ORC) career site fetches to legitimate *.fa.*.oraclecloud.com hosts. Unlike simpler providers, the trusted-host check uses a regex (ORACLE_HOST_RE) because Oracle career sites have tenant-specific subdomains.","triggerScenarios":"Called with a value new URL() cannot parse: undefined, empty string, a URL with spaces, or a schemeless path. The guard is invoked from fetch() before each page request (line: assertOracleUrl(apiUrl)), so it fires if buildApiUrl() produces a malformed URL — though buildApiUrl constructs from a validated host, so the more likely path is a direct/test call with bad input.","commonSituations":"A portals.yml oraclecloud entry has api or careers_url left empty or mistyped. A programmatic entry construction passes a non-string. Testing with a relative or fixture path. The resolveSite() function returned a host that, combined with buildApiUrl's path segments, produces an unparseable URL (unlikely but possible if the host contains illegal characters).","solutions":["Inspect the url argument: log it before assertOracleUrl to see the malformed value.","Ensure the portals.yml oraclecloud entry has a valid https:// careers_url or api in the form https://<tenant>.fa.<region>.oraclecloud.com/hcmUI/CandidateExperience/...","If calling fetch() directly, verify resolveSite(entry) returns a valid site object first."],"exampleFix":"// before — entry has a malformed URL\njob_boards:\n  oracle:\n    provider: oraclecloud\n    careers_url: 'oraclecloud.com/hcmUI/...'  // missing https:// and tenant\n\n// after\njob_boards:\n  oracle:\n    provider: oraclecloud\n    careers_url: 'https://acme.fa.eu.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1'","handlingStrategy":"validation","validationCode":"/** Validate a URL string is parseable before passing to assertOracleUrl. */\nfunction isValidUrlString(url) {\n  return typeof url === 'string'\n    && url.length > 0\n    && (() => { try { new URL(url); return true; } catch { return false; } })();\n}\n\nif (!isValidUrlString(entry.api) && !isValidUrlString(entry.careers_url)) {\n  console.warn(`oraclecloud entry ${entry.name} has no valid URL`);\n  continue;\n}","typeGuard":"/** @param {unknown} url @returns {url is string} */\nfunction isParseableUrl(url) {\n  if (typeof url !== 'string' || !url) return false;\n  try { new URL(url); return true; } catch { return false; }\n}","tryCatchPattern":"try {\n  await oracleProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('oraclecloud: invalid URL')) {\n    console.warn(`skipping oraclecloud entry ${entry.name}: malformed URL`);\n    continue;\n  }\n  throw err;\n}","preventionTips":["Validate URL fields in portals.yml at config-load time.","Ensure Oracle HCM career URLs include the full https://<tenant>.fa.<region>.oraclecloud.com path.","Call detect(entry) and skip null results before calling fetch()."],"tags":["url-validation","ssrf-guard","oraclecloud","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}