{"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":null,"httpStatus":null,"severity":"error","filePath":"providers/ashby.mjs","lineNumber":89,"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":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/ashby.mjs#L71-L107","documentation":"The Ashby provider validates every board URL before use. assertAshbyUrl first parses the string with the URL constructor; if parsing fails (malformed URL, empty string, whitespace, missing scheme), it throws this error. It is the first gate in a chain of checks (parseability, HTTPS, hostname allowlist).","triggerScenarios":"Calling assertAshbyUrl (directly or via the provider's fetch path) with a string that `new URL()` cannot parse — e.g. `jobs.ashby.co/board` without scheme, an empty string, a URL containing stray spaces or control characters, or a portals.yml entry whose api/careers URL is malformed.","commonSituations":"Hand-edited portals.yml entries with typos, URLs pasted with leading/trailing whitespace or quotes, relative URLs taken from a page instead of absolute ones, or template placeholders like `${TENANT}` left unexpanded.","solutions":["Print/inspect the exact offending URL value; check for missing `https://` scheme, stray whitespace, or unexpanded placeholders.","Trim and normalize the input: `url.trim()` and prepend `https://` only if an absolute host is present but no scheme.","Fix the offending portals.yml entry so the api/careers URL is a full absolute HTTPS URL.","If building URLs programmatically, use `new URL(path, base)` with a validated base instead of string concatenation."],"exampleFix":"// before\nconst url = 'jobs.ashby.co/exampleco/postings';\nassertAshbyUrl(url); // throws\n\n// after\nconst url = 'https://jobs.ashby.co/exampleco/postings';\nassertAshbyUrl(url); // passes scheme check","handlingStrategy":"validation","validationCode":"function isValidAshbyUrl(url) {\n  if (typeof url !== 'string' || !url.trim()) return false;\n  try { new URL(url.trim()); return true; } catch { return false; }\n}","typeGuard":"function isParsedUrl(value) {\n  try { return { ok: true, url: new URL(value) }; } catch { return { ok: false }; }\n}","tryCatchPattern":"try {\n  assertAshbyUrl(entry.api);\n} catch (err) {\n  console.warn(`Skipping entry ${entry.name}: ${err.message}`);\n  return null;\n}","preventionTips":["Store only full absolute HTTPS URLs in portals.yml — never bare hosts or relative paths.","Trim and quote-strip values pasted into config files.","Add a config lint step that runs `new URL()` over every entry at load time.","Expand and check template placeholders before committing config."],"tags":["url","validation","ashby","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"}