{"record":{"id":"385c711ce66f8654","repo":"santifer/career-ops","slug":"breezy-url-must-use-https-url","errorCode":null,"errorMessage":"breezy: URL must use HTTPS: ${url}","messagePattern":"breezy: URL must use HTTPS: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/breezy.mjs","lineNumber":26,"sourceCode":"// approach as the recruitee / bamboohr providers).\n//\n// Breezy boards expose every published position as a public JSON array at\n// `<tenant>.breezy.hr/json` — title, absolute url, location, and a published\n// date, all in the list payload at zero token cost (no per-job request, so the\n// scanner stays zero-token). Breezy's authenticated REST API (api.breezy.hr) is\n// intentionally NOT used; only the public board feed.\n\nconst BREEZY_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.breezy\\.hr$/;\n\n/** @param {string} url */\nfunction assertBreezyUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`breezy: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`breezy: URL must use HTTPS: ${url}`);\n  if (!BREEZY_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`breezy: untrusted hostname \"${parsed.hostname}\" — must match <tenant>.breezy.hr`);\n  }\n  return url;\n}\n\n/**\n * Resolve the tenant origin (`https://<tenant>.breezy.hr`) from an entry.\n * Honours an explicit `api:` URL, else parses `careers_url`.\n * @param {import('./_types.js').PortalEntry} entry\n * @returns {string | null}\n */\nfunction resolveOrigin(entry) {\n  const rawApi = typeof entry.api === 'string' ? entry.api : '';\n  const rawCareers = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  const raw = (rawApi || rawCareers).trim();\n  if (!raw) return null;\n  let parsed;","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/breezy.mjs#L8-L44","documentation":"assertBreezyUrl throws this when the URL parses but its protocol is not 'https:'. The provider only talks to Breezy tenant boards over TLS, both to protect the scrape and as part of the SSRF defence (an http: origin could be intercepted or redirected). Any http:// or other-scheme URL is rejected before fetching.","triggerScenarios":"A portals.yml entry configured with 'http://acme.breezy.hr/json' or an api: field using a non-https scheme (ftp:, //protocol-relative resolved oddly, etc.) reaches assertBreezyUrl via provider fetch().","commonSituations":"Copying a URL from a browser that downgraded to http; writing 'http://' by habit in local config; a config migration that stripped the 's'; protocol-relative '//acme.breezy.hr' strings which fail earlier or resolve with page scheme.","solutions":["Change the scheme in the portals.yml entry to https:// (e.g. 'https://acme.breezy.hr').","Confirm the tenant board actually serves HTTPS — all Breezy HR boards do, so plain http is always a config mistake.","If the URL comes from code, hardcode the 'https://' prefix when composing the origin rather than accepting caller input."],"exampleFix":"// before\nconst apiUrl = `http://${tenant}.breezy.hr/json`;\n// after\nconst apiUrl = `https://${tenant}.breezy.hr/json`;","handlingStrategy":"validation","validationCode":"function isHttpsUrl(url) {\n  try { return new URL(url).protocol === 'https:'; } catch { return false; }\n}\nif (!isHttpsUrl(entry.careers_url)) throw new Error(`config: careers_url must be https: ${entry.careers_url}`);","typeGuard":"function isHttpsProtocol(url) { try { return new URL(url).protocol === 'https:'; } catch { return false; } }","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('breezy: URL must use HTTPS')) {\n    console.warn(`Upgrading ${entry.name} to https and retrying once`);\n    return provider.fetch({ ...entry, careers_url: entry.careers_url.replace(/^http:/, 'https:') }, ctx);\n  }\n  throw err;\n}","preventionTips":["Normalize http:// to https:// once at config-load time instead of per-fetch.","Never accept caller-supplied schemes; compose URLs from a hardcoded https:// prefix.","Add a startup check that rejects any non-https careers_url in portals.yml."],"tags":["url","https","security","config"],"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"}