{"record":{"id":"b6589393512cbf0a","repo":"santifer/career-ops","slug":"ashby-invalid-url-url","errorCode":null,"errorMessage":"ashby: invalid URL: ${url}","messagePattern":"ashby: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/ashby.mjs","lineNumber":86,"sourceCode":"  // Ensure correct ordering (min <= max)\n  const resolvedMin = /** @type {number} */ (min ?? max);\n  const resolvedMax = /** @type {number} */ (max ?? min);\n  return {\n    min: Math.min(resolvedMin, resolvedMax),\n    max: Math.max(resolvedMin, resolvedMax),\n    currency: currency.toUpperCase(),\n  };\n}\n\nconst ALLOWED_ASHBY_HOSTS = new Set(['api.ashbyhq.com']);\n\n/** @param {string} url */\nfunction assertAshbyUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`ashby: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`ashby: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_ASHBY_HOSTS.has(parsed.hostname))\n    throw new Error(`ashby: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_ASHBY_HOSTS].join(', ')}`);\n  return url;\n}\n\n/** @param {import('./_types.js').PortalEntry} entry */\nfunction resolveApiUrl(entry) {\n  // Explicit api: wins — lets an entry keep a human-facing corporate\n  // careers_url (e.g. https://openai.com/careers) while still pinning the\n  // Ashby posting-api board (mirrors greenhouse's api: precedence).\n  if (entry.api) {\n    assertAshbyUrl(entry.api);\n    return entry.api;\n  }\n  const url = entry.careers_url || '';\n  const match = url.match(/jobs\\.ashbyhq\\.com\\/([^/?#]+)/);","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/ashby.mjs#L68-L104","documentation":"ashby validates every URL through `assertAshbyUrl` before fetching. The 'invalid URL' variant fires when `new URL(url)` throws — the input is not parseable as an absolute URL. This is the first of three sequential guards (parse → https → host allowlist).","triggerScenarios":"`assertAshbyUrl(url)` receives a string `new URL()` cannot parse: a schemeless host (`api.ashbyhq.com/...`), a protocol-relative URL, an empty string, or a value with illegal characters. Typically called on `entry.api` or a derived URL in `resolveApiUrl`.","commonSituations":"An entry's `api:` field is set without a scheme, a careers_url that gets parsed drops characters, or a templated/config-injected value is malformed.","solutions":["Set `api:` to a fully-qualified `https://api.ashbyhq.com/...` URL.","Validate entry URLs at config load time so the error surfaces next to the offending row.","Check for whitespace or template interpolation producing an empty/mangled scheme."],"exampleFix":"# before\n- name: Acme\n  api: api.ashbyhq.com/posting-api?compId=acme\n\n# after\n- name: Acme\n  api: https://api.ashbyhq.com/posting-api?compId=acme","handlingStrategy":"validation","validationCode":"// Validate entry.api is an absolute URL before passing to the provider\nfunction isValidAbsoluteUrl(u) {\n  try { new URL(u); return true; } catch { return false; }\n}\nif (entry.api && !isValidAbsoluteUrl(entry.api)) {\n  throw new Error(`ashby: entry ${entry.name} has a malformed api URL: ${entry.api}`);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Require fully-qualified URLs in config (no schemeless hosts).","Run a config validator that tests every entry.api with `new URL()`.","Keep `detect()` as the first gate so malformed entries are skipped rather than fetched."],"tags":["ashby","url-validation","ssrf","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}