{"record":{"id":"0023ca7b811f9fe6","repo":"santifer/career-ops","slug":"ashby-url-must-use-https-url","errorCode":null,"errorMessage":"ashby: URL must use HTTPS: ${url}","messagePattern":"ashby: URL must use HTTPS: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/ashby.mjs","lineNumber":91,"sourceCode":"  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\\/([^/?#]+)/);\n  if (!match) return null;\n  return `https://api.ashbyhq.com/posting-api/job-board/${match[1]}?includeCompensation=true`;","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/ashby.mjs#L73-L109","documentation":"After a URL parses successfully, assertAshbyUrl rejects any URL whose protocol is not https. The Ashby provider deliberately forbids http:// (and any other scheme) so credentials and board data are never sent over plaintext and no downgrade redirect tricks are possible.","triggerScenarios":"Calling assertAshbyUrl with a parsed-valid URL using `http://` (e.g. `http://jobs.ashby.co/...`), or any other scheme like `ftp://` or a custom scheme that still parses.","commonSituations":"Old or hand-copied config entries pointing at plain http, internal proxy-style URLs like `http://localhost:8080`, or a config where the scheme got stripped/mangled by a YAML parser or string templating.","solutions":["Change the URL scheme to `https://` in the portals.yml entry or calling code.","If the target only serves http, it is unsupported — find the canonical HTTPS Ashby board URL (`https://jobs.ashby.co/<org>` or the `jobs.ashbyhq.com` endpoint).","Pre-normalize inputs: `if (url.startsWith('http://')) url = 'https://' + url.slice(7)` only when you control the source and know HTTPS is served.","Check for double-scheme mistakes like `https://http://...` produced by concatenation."],"exampleFix":"// before\nassertAshbyUrl('http://jobs.ashby.co/exampleco'); // throws\n\n// after\nassertAshbyUrl('https://jobs.ashby.co/exampleco'); // ok","handlingStrategy":"validation","validationCode":"function isHttpsUrl(url) {\n  try { return new URL(url).protocol === 'https:'; } catch { return false; }\n}","typeGuard":"function asHttpsUrl(value) {\n  const u = new URL(value); // caller ensures parseable\n  return u.protocol === 'https:' ? u : null;\n}","tryCatchPattern":"try {\n  assertAshbyUrl(url);\n} catch (err) {\n  if (/must use HTTPS/.test(err.message)) {\n    url = url.replace(/^http:/, 'https:');\n    assertAshbyUrl(url);\n  } else throw err;\n}","preventionTips":["Always use https:// in job-board config; all major ATS hosts serve HTTPS.","Normalize http:// to https:// at config load for known ATS hosts.","Watch for string concatenation that drops or mangles the scheme.","Add an https-only check to your config lint."],"tags":["url","https","security","ashby"],"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"}