{"record":{"id":"c3ef00235e940af1","repo":"santifer/career-ops","slug":"csod-no-anonymous-token-on-cfg-homeurl","errorCode":null,"errorMessage":"csod: no anonymous token on ${cfg.homeUrl}","messagePattern":"csod: no anonymous token on (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/csod.mjs","lineNumber":207,"sourceCode":"    // 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' } });\n      const setCookies = typeof res?.headers?.getSetCookie === 'function' ? res.headers.getSetCookie() : [];\n      cookie = cookieHeaderFrom(setCookies);\n      html = await res.text();\n    } else {\n      html = await ctx.fetchText(cfg.homeUrl, { redirect: 'error', headers: { accept: 'text/html' } });\n    }\n    const token = extractToken(html);\n    if (!token) throw new Error(`csod: no anonymous token on ${cfg.homeUrl}`);\n\n    const wait = (ms) => (ctx.sleep ? ctx.sleep(ms) : new Promise((r) => setTimeout(r, ms)));\n    const maxPages = resolveMaxPages(entry);\n    const jobs = [];\n    const seen = new Set();\n    let total = null;\n\n    for (let page = 1; page <= maxPages; page++) {\n      if (page > 1) await wait(PAGE_DELAY_MS);\n      const json = await ctx.fetchJson(cfg.searchApi, {\n        method: 'POST',\n        redirect: 'error',\n        headers: {\n          'content-type': 'application/json',\n          accept: 'application/json',\n          authorization: `Bearer ${token}`,\n          ...(cookie ? { cookie } : {}),\n        },","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/csod.mjs#L189-L225","documentation":"The Cornerstone OnDemand (CSOD) provider bootstraps each fetch by GETting the tenant's career-site home page, which embeds an anonymous bearer JWT (\"token\":\"eyJ…\") plus session cookies needed by the public search API. When `extractToken(html)` finds no token in that bootstrap HTML, the provider throws because the subsequent POST to /services/x/career-site/v1/search cannot authenticate. This almost always means the fetched page is not the expected ~5 KB bootstrap document.","triggerScenarios":"fetch() completed the GET of cfg.homeUrl (either via ctx.fetch with cookies, or ctx.fetchText with redirect:'error') and extractToken() on the response body returned falsy — i.e. the HTML contains no embedded `\"token\":\"…\"` anonymous JWT. Happens when the homeUrl points at the branded corporate careers page instead of the *.csod.com careersite URL, when the site redirects to a login/consent/captcha page, or when the tenant has changed its page structure so the token is no longer inline.","commonSituations":"Portals.yml entry where `careers_url` is the company's branded careers page and no `api:` field carries the https://{tenant}.csod.com/ux/ats/careersite/{siteId}/home?c={corpName} URL; a tenant that now gates anonymous access behind a cookie-consent interstitial; a wrong siteId or corpName query param landing on an error page; CSOD changing the bootstrap format in an upgrade.","solutions":["Fix the portal entry so `api:` (or careers_url) points at the real *.csod.com URL matching /ux/ats/careersite/\\d+/home?c=<corpName>, not the branded corporate page.","Open cfg.homeUrl in a browser and inspect the page source for \"token\":\"eyJ — if absent, the tenant gates the board; remove or replace the entry.","Verify the siteId and c= corpName parameters are correct (wrong values land on error/redirect pages without a token).","Check the provider version against the tenant's current page markup — if CSOD renamed the embedded token field, extractToken must be updated.","If a redirect is silently followed to a login page, ensure the fetch keeps redirect:'error' semantics so the misconfiguration surfaces before token extraction."],"exampleFix":"// before (portals.yml)\n- name: ohb\n  careers_url: https://www.ohb.de/karriere\n// after\n- name: ohb\n  careers_url: https://www.ohb.de/karriere\n  api: https://career-ohb.csod.com/ux/ats/careersite/4/home?c=career-ohb","handlingStrategy":"validation","validationCode":"// validate the entry before scanning\nconst u = new URL(entry.api || entry.careers_url);\nif (!(u.protocol === 'https:' && (u.host === 'csod.com' || u.host.endsWith('.csod.com')) &&\n      /\\/ux\\/ats\\/careersite\\/\\d+\\//.test(u.pathname))) {\n  throw new Error('entry must point at https://<tenant>.csod.com/ux/ats/careersite/<id>/home?c=<corp>');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.startsWith('csod: no anonymous token')) {\n    // board gated or wrong URL — flag entry for manual review, don't retry\n    reportUnreachable(entry, e.message);\n  } else throw e;\n}","preventionTips":["Always set `api:` to the raw *.csod.com careersite URL, keeping the branded page in careers_url only.","Verify the careersite path shape /ux/ats/careersite/{id}/home?c={corpName} when adding a new CSOD tenant.","Open the homeUrl in a browser and confirm an anonymous session (no login wall) sees job listings.","Re-check tenants periodically: consent interstitials and CSOD upgrades can remove the embedded token."],"tags":["network","authentication","config","scraping"],"backgroundTag":"authentication-required","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"}