{"record":{"id":"e40decd48c929e79","repo":"santifer/career-ops","slug":"csod-cannot-resolve-careersite-url-for-entry-na","errorCode":null,"errorMessage":"csod: cannot resolve careersite URL for ${entry.name}","messagePattern":"csod: cannot resolve careersite URL for (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/csod.mjs","lineNumber":181,"sourceCode":"  if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES);\n  return MAX_PAGES;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'csod',\n\n  detect(entry) {\n    const url = entry.api || entry.careers_url || '';\n    if (typeof url !== 'string') return null;\n    // Host check (not a path substring) so evil.com/x.csod.com can't spoof it,\n    // and the URL must carry the careersite path shape we know how to drive.\n    return resolveConfig({ api: url }) ? { url } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const cfg = resolveConfig(entry);\n    if (!cfg) throw new Error(`csod: cannot resolve careersite URL for ${entry.name}`);\n\n    // The bootstrap page yields two things, not one: the anonymous bearer\n    // token, and — on some tenants — the session cookies the search API\n    // insists on. careers-kln rejects an otherwise valid token+body with\n    // \"HTTP 401 CSOD Unauthorized\" until those cookies come back with it, so\n    // the token alone is not a sufficient credential. Prefer ctx.fetchResponse\n    // to see Set-Cookie; fall back to fetchText when the caller's ctx predates\n    // it (older embedders and test mocks), which keeps the pre-cookie\n    // behaviour intact for tenants that never needed it.\n    //\n    // cfg.homeUrl and cfg.searchApi are both built from the same parsed\n    // origin, so replaying these cookies cannot reach a third-party host.\n    // redirect:'error' on the bootstrap keeps that true: origin validation\n    // covers the URL we ask for, not wherever a 3xx would send us.\n    let html;\n    let cookie = '';\n    if (typeof ctx.fetchResponse === 'function') {\n      const res = await ctx.fetchResponse(cfg.homeUrl, { redirect: 'error', headers: { accept: 'text/html' } });","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/csod.mjs#L163-L199","documentation":"The CSOD (Cornerstone OnDemand) provider's fetch calls resolveConfig(entry) to obtain a careersite URL configuration it knows how to drive; when the entry yields no such config it throws this error. The provider needs the careersite path shape to bootstrap the anonymous bearer token and session cookies, so an unresolvable URL is a hard stop before any network call. It indicates the entry's csod configuration is absent or malformed.","triggerScenarios":"Scanning an entry routed to the csod provider whose careersite/api URL is missing or does not match the expected careersite path shape, so resolveConfig returns null.","commonSituations":"Configuring a Cornerstone-hosted company with a generic careers-page URL that lacks the csod careersite path shape; omitting the url/api key on the entry; a tenant changing their careersite URL structure so the previously working entry no longer resolves; typo-ing the config key.","solutions":["Set the entry's careersite URL to the full CSOD careersite URL with the expected path shape (compare with a working csod entry in portals.yml).","Verify the URL matches the careersite path shape resolveConfig expects — inspect resolveConfig in providers/csod.mjs for the exact shape test.","If the tenant's URL structure changed, rediscover the new careersite URL from the live site and update the entry.","Confirm the config key name (url/api) matches what resolveConfig reads — a misnamed key yields null and this error."],"exampleFix":"// before\n- name: mycompany\n  provider: csod\n// after\n- name: mycompany\n  provider: csod\n  url: https://mycompany.csod.com/uxp/career-site/...","handlingStrategy":"validation","validationCode":"if (entry.provider === 'csod' && !entry.url) { throw new Error(`csod entry '${entry.name}' is missing the careersite url`); }","typeGuard":"const hasCsodUrl = (entry) => typeof entry.url === 'string' && entry.url.startsWith('https://') && entry.url.includes('.csod.com/');","tryCatchPattern":"try {\n  const jobs = await provider.fetch(entry, ctx);\n} catch (err) {\n  if (err.message.startsWith('csod: cannot resolve careersite URL')) {\n    console.warn(`Skipping ${entry.name}: careersite URL missing or wrong path shape`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Copy the exact careersite URL (with the expected path shape) from a working csod entry","Check resolveConfig's shape test in providers/csod.mjs before adding a new csod tenant","Re-verify csod URLs if a tenant migrates their careersite structure","Validate config keys (url/api) against what resolveConfig reads when onboarding a new entry"],"tags":["config","missing-config-field","csod","url-validation"],"backgroundTag":"missing-required-config-field","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}