{"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":"Error","httpStatus":null,"severity":"error","filePath":"providers/greenhouse.mjs","lineNumber":22,"sourceCode":"// Greenhouse provider — hits the public boards-api JSON endpoint.\n// Handles both explicit `api:` URLs and auto-detection from `careers_url`.\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":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/greenhouse.mjs#L4-L40","documentation":"greenhouse.mjs throws this in assertGreenhouseUrl() after the URL parses but its protocol is not 'https:'. It enforces TLS for the Greenhouse boards API. Because the guard covers the operator-supplied entry.api (and the derived careers_url-built URL), a live throw usually comes from an http:// api: value in portals.yml.","triggerScenarios":"entry.api is set to an http:// URL (e.g. for local proxying); a careers_url used http:// and was the basis for a derived http URL in a stale code path; a test calls assertGreenhouseUrl('http://boards-api.greenhouse.io/...').","commonSituations":"Local debugging through a non-TLS intercepting proxy; a copy-paste of an old http Greenhouse link; an env override downgraded the scheme.","solutions":["Set api to its https:// form or remove api to let the provider derive an HTTPS URL from careers_url.","For local interception, trust a proxy CA against the HTTPS endpoint instead of downgrading.","Remove tests that pass http URLs unless they assert this specific throw."],"exampleFix":"# before\n- name: Acme\n  provider: greenhouse\n  api: http://boards-api.greenhouse.io/v1/boards/acme/jobs\n\n# after\n- name: Acme\n  provider: greenhouse\n  api: https://boards-api.greenhouse.io/v1/boards/acme/jobs","handlingStrategy":"validation","validationCode":"// Reject non-HTTPS Greenhouse api values before they reach the provider.\nfunction greenhouseApiIsHttps(entry) {\n  if (!entry.api) return true; // derived URL is always https\n  try { return new URL(entry.api).protocol === 'https:'; } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never set api to http://; use a trusted proxy CA against the HTTPS endpoint for local interception.","Add a lint rule forbidding http: in any provider api: field.","Prefer careers_url derivation (which always yields https) over a hand-written api."],"tags":["url-validation","tls","greenhouse","config","portals-yml"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}