{"record":{"id":"686fd092a5ba9a5d","repo":"santifer/career-ops","slug":"bamboohr-invalid-url-url","errorCode":null,"errorMessage":"bamboohr: invalid URL: ${url}","messagePattern":"bamboohr: invalid URL: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/bamboohr.mjs","lineNumber":25,"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":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/bamboohr.mjs#L7-L43","documentation":"The BambooHR provider's assertBambooHRUrl first checks that the string parses as a URL via `new URL()`; unparseable input throws this error. It mirrors the Ashby gate: parseability, then HTTPS, then hostname pattern, in that order.","triggerScenarios":"Calling assertBambooHRUrl (or the fetch path that builds `<origin>/careers/list` and validates it) with a string `new URL()` rejects — empty string, missing scheme like `mycompany.bamboohr.com`, whitespace, or a malformed origin built from a bad tenant name.","commonSituations":"portals.yml BambooHR entries with just a tenant subdomain instead of the full URL, placeholders like `<tenant>.bamboohr.com` left literally in config, or an empty tenant value producing an empty/invalid origin string.","solutions":["Inspect the exact URL value; ensure it is a full absolute URL including the scheme.","Use `https://<tenant>.bamboohr.com` as the origin form in config, not a bare tenant name.","Trim whitespace and expand any template placeholders in the config entry.","Build origins with `new URL('https://' + tenant + '.bamboohr.com')` and validate the tenant name first (no slashes/spaces)."],"exampleFix":"// before\nconst origin = 'mycompany.bamboohr.com';\nassertBambooHRUrl(`${origin}/careers/list`); // throws: not parseable\n\n// after\nconst origin = 'https://mycompany.bamboohr.com';\nassertBambooHRUrl(`${origin}/careers/list`); // ok","handlingStrategy":"validation","validationCode":"function isValidBambooUrl(url) {\n  if (typeof url !== 'string' || !url.trim()) return false;\n  try { new URL(url.trim()); return true; } catch { return false; }\n}","typeGuard":"function parseUrlSafe(value) {\n  try { return { ok: true, url: new URL(value) }; } catch { return { ok: false }; }\n}","tryCatchPattern":"try {\n  assertBambooHRUrl(apiUrl);\n} catch (err) {\n  console.warn(`Skipping BambooHR entry: ${err.message}`);\n  return null;\n}","preventionTips":["Store full `https://<tenant>.bamboohr.com` origins, never bare tenant names.","Expand placeholders like `<tenant>` before running the scanner.","Trim config values and validate them at load with `new URL()`.","Validate tenant names are simple subdomain labels (letters/digits/hyphens)."],"tags":["url","validation","bamboohr","config"],"backgroundTag":"invalid-url","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"}