{"record":{"id":"a06a50c7ba0ead2e","repo":"santifer/career-ops","slug":"bamboohr-cannot-derive-api-url-for-entry-name","errorCode":null,"errorMessage":"bamboohr: cannot derive API URL for ${entry.name}","messagePattern":"bamboohr: cannot derive API URL for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/bamboohr.mjs","lineNumber":65,"sourceCode":"    return null;\n  }\n  if (parsed.protocol !== 'https:') return null;\n  if (!BAMBOOHR_HOST_RE.test(parsed.hostname)) return null;\n  return `https://${parsed.hostname}`;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'bamboohr',\n\n  detect(entry) {\n    const origin = resolveOrigin(entry);\n    return origin ? { url: `${origin}/careers/list` } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const origin = resolveOrigin(entry);\n    if (!origin) throw new Error(`bamboohr: cannot derive API URL for ${entry.name}`);\n    const apiUrl = `${origin}/careers/list`;\n    assertBambooHRUrl(apiUrl);\n    // redirect:'error' + the host check above keep the final hostname pinned to\n    // the tenant — a server-side redirect can't bounce us off-domain (SSRF).\n    const json = /** @type {any} */ (await ctx.fetchJson(apiUrl, { redirect: 'error' }));\n    return parseBambooHRResponse(json, entry.name, origin);\n  },\n};\n\n/**\n * Parse a BambooHR `/careers/list` response. Exported for unit tests.\n *\n * BambooHR returns:\n *   { meta: {...}, result: [{ id, jobOpeningName,\n *       location: { city?, state? }, isRemote?, employmentStatusLabel? }] }\n *\n * - url: built as `<origin>/careers/<id>` — matches the public\n *   `jobOpeningShareUrl`. Rows without a non-empty `id` are dropped (no stable","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/bamboohr.mjs#L47-L83","documentation":"BambooHR's `resolveOrigin` returns null when neither `entry.api` nor `entry.careers_url` yields a `<tenant>.bamboohr.com` origin. `fetch` then throws, naming the entry. `detect()` returns null on the same condition (skipping the provider); this throw only fires if `fetch` is called directly.","triggerScenarios":"`resolveOrigin(entry)` returns null — the entry has no `api:` and no `careers_url` on a bamboohr.com host. Thrown at the top of `fetch` before any network call.","commonSituations":"An entry declares `provider: bamboohr` but only carries a corporate careers_url, the bamboohr fields were omitted, or a YAML indentation error orphaned them.","solutions":["Add `api:` or `careers_url:` pointing to `https://<tenant>.bamboohr.com`.","Verify the company actually uses BambooHR before keeping the provider.","Use `detect()` before `fetch` so unconfigured entries are skipped rather than throwing."],"exampleFix":"# before\n- name: Acme\n  provider: bamboohr\n  careers_url: https://careers.acme.com/\n\n# after\n- name: Acme\n  provider: bamboohr\n  careers_url: https://acme.bamboohr.com/","handlingStrategy":"type-guard","validationCode":"// Use detect() before fetch; skip entries that don't resolve\nconst detected = provider.detect(entry);\nif (!detected) {\n  console.warn(`bamboohr: skipping ${entry.name} — no <tenant>.bamboohr.com origin (add api/careers_url)`);\n  continue;\n}","typeGuard":"const RE = /^[a-z0-9][a-z0-9-]*\\.bamboohr\\.com$/;\n/** @param {any} e */\nfunction hasBambooOrigin(e) {\n  const u = e?.api ?? e?.careers_url;\n  if (typeof u !== 'string') return false;\n  try { return RE.test(new URL(u).hostname); } catch { return false; }\n}","tryCatchPattern":"try {\n  const origin = resolveOrigin(entry);\n  if (!origin) throw new Error(`bamboohr: cannot derive API URL for ${entry.name}`);\n  // ...fetch\n} catch (err) {\n  if (/cannot derive API URL/.test(err.message)) { console.warn(err.message); return []; }\n  throw err;\n}","preventionTips":["Always gate fetch with detect().","Validate api/careers_url against the tenant regex at config load.","Flag entries declaring bamboohr but lacking a bamboohr.com URL in a config report."],"tags":["bamboohr","config","missing-field","host-derivation"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}