{"record":{"id":"686fd092a5ba9a5d","repo":"santifer/career-ops","slug":"bamboohr-invalid-url-url","errorCode":null,"errorMessage":"bamboohr: invalid URL: ${url}","messagePattern":"bamboohr: invalid URL: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/bamboohr.mjs","lineNumber":23,"sourceCode":"// Auto-detects from careers_url pattern `https://<tenant>.bamboohr.com[/...]`.\n// Per-tenant subdomains are the variable part, so SSRF defence uses a regex\n// match on `<safe-tenant>.bamboohr.com` rather than a static allowlist\n// (same approach as the recruitee provider).\n//\n// The list endpoint (`/careers/list`) returns lightweight metadata — enough for\n// the Job contract (title, url, location) at zero token cost. The full JD lives\n// behind a second `/careers/<id>/detail` request, which the scanner deliberately\n// skips to stay zero-token (so `description`/`postedAt` are omitted).\n\nconst BAMBOOHR_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.bamboohr\\.com$/;\n\n/** @param {string} url */\nfunction assertBambooHRUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`bamboohr: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`bamboohr: URL must use HTTPS: ${url}`);\n  if (!BAMBOOHR_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`bamboohr: untrusted hostname \"${parsed.hostname}\" — must match <tenant>.bamboohr.com`);\n  }\n  return url;\n}\n\n/**\n * Resolve the tenant origin (`https://<tenant>.bamboohr.com`) from an entry.\n * Honours an explicit `api:` URL, else parses `careers_url`.\n * @param {import('./_types.js').PortalEntry} entry\n * @returns {string | null}\n */\nfunction resolveOrigin(entry) {\n  const rawApi = typeof entry.api === 'string' ? entry.api : '';\n  const rawCareers = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  const raw = (rawApi || rawCareers).trim();","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/bamboohr.mjs#L5-L41","documentation":"BambooHR validates URLs via `assertBambooHRUrl`. The 'invalid URL' variant fires when `new URL(url)` throws — the string is not a parseable absolute URL. First of three sequential guards before any fetch.","triggerScenarios":"`assertBambooHRUrl(url)` is called (typically on the derived `${origin}/careers/list` URL or an `api:` field) with a value `new URL()` cannot parse: schemeless host, protocol-relative, empty, or illegal characters.","commonSituations":"An `api:` field set without `https://`, a careers_url with a stray space or broken template, or config injection that mangles the scheme.","solutions":["Provide a full `https://<tenant>.bamboohr.com` URL in `api:` or `careers_url`.","Validate entry URLs at config load so the error points at the offending row.","Strip accidental whitespace from templated URL fields."],"exampleFix":"# before\n- name: Acme\n  api: acme.bamboohr.com\n\n# after\n- name: Acme\n  api: https://acme.bamboohr.com","handlingStrategy":"validation","validationCode":"function isValidAbsoluteUrl(u) {\n  try { new URL(u); return true; } catch { return false; }\n}\nfor (const f of ['api', 'careers_url']) {\n  if (entry[f] && !isValidAbsoluteUrl(entry[f])) {\n    throw new Error(`bamboohr: entry ${entry.name} has malformed ${f}: ${entry[f]}`);\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Require fully-qualified URLs in config entries.","Run `new URL()` over every api/careers_url at startup.","Use detect() so malformed entries are skipped rather than thrown."],"tags":["bamboohr","url-validation","ssrf","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}