{"record":{"id":"dd769a534f54bd46","repo":"santifer/career-ops","slug":"greenhouse-url-must-use-https-url","errorCode":null,"errorMessage":"greenhouse: URL must use HTTPS: ${url}","messagePattern":"greenhouse: URL must use HTTPS: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/greenhouse.mjs","lineNumber":28,"sourceCode":"\nimport { htmlToText } from './_html-to-text.mjs';\n\nconst ALLOWED_GREENHOUSE_HOSTS = new Set([\n  'boards-api.greenhouse.io',\n  'boards.greenhouse.io',\n  'job-boards.greenhouse.io',\n  'job-boards.eu.greenhouse.io',\n]);\n\n/** @param {string} url */\nfunction assertGreenhouseUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`greenhouse: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`greenhouse: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_GREENHOUSE_HOSTS.has(parsed.hostname))\n    throw new Error(`greenhouse: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_GREENHOUSE_HOSTS].join(', ')}`);\n  return url;\n}\n\n/** @param {import('./_types.js').PortalEntry} entry */\nfunction resolveApiUrl(entry) {\n  if (entry.api) {\n    assertGreenhouseUrl(entry.api);\n    return entry.api;\n  }\n  const url = entry.careers_url || '';\n  const match = url.match(/job-boards(?:\\.eu)?\\.greenhouse\\.io\\/([^/?#]+)/);\n  if (match) return `https://boards-api.greenhouse.io/v1/boards/${match[1]}/jobs`;\n  return null;\n}\n\n// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/greenhouse.mjs#L10-L46","documentation":"assertGreenhouseUrl enforces that every Greenhouse API/board URL uses HTTPS. If the parsed URL has any other protocol (http:, ftp:, etc.), the provider throws this error. This prevents accidentally hitting the Greenhouse API in plaintext and keeps all outbound requests on TLS.","triggerScenarios":"A portals.yml api: or careers_url value starting with http:// instead of https://, or a URL built from a base string that had its scheme stripped and re-prefixed as http.","commonSituations":"Config copied from old documentation that predated HTTPS-only boards, a reverse-proxy note telling you to use http:// for local debugging, or a user-typed URL without scheme defaults mis-resolved to http.","solutions":["Change the entry's scheme to https:// in portals.yml.","If you maintain a URL-builder helper, hardcode the https: scheme instead of copying the input scheme.","Run a config lint that rejects non-https URLs for ATS providers before scanning."],"exampleFix":"// before (portals.yml)\napi: http://boards-api.greenhouse.io/acme\n// after\napi: https://boards-api.greenhouse.io/acme","handlingStrategy":"validation","validationCode":"function isHttps(url) {\n  try { return new URL(url).protocol === 'https:'; } catch { return false; }\n}\nif (!isHttps(entry.api)) throw new Error(`greenhouse entries must use https: ${entry.api}`);","typeGuard":"const isHttpsUrl = (s) => { try { return new URL(s).protocol === 'https:'; } catch { return false; } };","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (/greenhouse: URL must use HTTPS/.test(err.message)) {\n    console.error(`Upgrade entry \"${entry.name}\" to https — see ${err.message}`);\n    return;\n  }\n  throw err;\n}","preventionTips":["Never copy http:// URLs from legacy docs into ATS config.","Enforce https in any URL-building helper.","Add a CI check rejecting non-https provider URLs.","Default to the provider's canonical https API URL."],"tags":["url","https","security","greenhouse"],"backgroundTag":"invalid-url-format","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"}