{"record":{"id":"e572f9f608fc93af","repo":"santifer/career-ops","slug":"breezy-invalid-url-url","errorCode":null,"errorMessage":"breezy: invalid URL: ${url}","messagePattern":"breezy: invalid URL: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/breezy.mjs","lineNumber":24,"sourceCode":"// Per-tenant subdomains are the variable part, so SSRF defence uses a regex\n// match on `<safe-tenant>.breezy.hr` rather than a static allowlist (same\n// 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();","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/breezy.mjs#L6-L42","documentation":"The Breezy HR provider's assertBreezyUrl guard throws this when the URL string cannot be parsed by the WHATWG URL constructor. Every request URL in the provider passes through this SSRF guard before fetching, so a malformed URL is rejected before any network I/O. It means the value (typically derived from a portals.yml entry's api or careers_url field) is not a syntactically valid absolute URL.","triggerScenarios":"Calling provider fetch/detect with a portals entry whose careers_url or api string is malformed: missing scheme ('acme.breezy.hr/json'), misspelled scheme ('htps://...'), whitespace/control characters in the URL, or a bare path. new URL() throws and the catch block rethrows as this error.","commonSituations":"Hand-editing portals.yml and omitting 'https://'; pasting a URL with a leading/trailing invisible character or non-breaking space; templating mistakes where a variable interpolates empty or partial, yielding '/json' or 'https:///json'.","solutions":["Open the portals.yml entry named in the error context and make the careers_url/api a full absolute URL: 'https://<tenant>.breezy.hr'.","Trim the value and re-check for stray whitespace, BOM, or non-ASCII lookalike characters around the URL.","Validate locally with `new URL(value)` in node before editing config to see the exact SyntaxError.","If the value is built programmatically, ensure the origin part is non-empty before appending '/json'."],"exampleFix":"// before\ncareers_url: acme.breezy.hr\n// after\ncareers_url: https://acme.breezy.hr","handlingStrategy":"validation","validationCode":"function isValidBreezyUrl(url) {\n  try { new URL(url); return true; } catch { return false; }\n}\nif (!isValidBreezyUrl(entry.careers_url)) throw new Error(`config: not a valid URL: ${entry.careers_url}`);","typeGuard":"function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; }","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('breezy: invalid URL')) {\n    console.warn(`Skipping ${entry.name}: malformed careers_url — fix portals.yml`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Always store full absolute https:// URLs in portals.yml, never bare hosts.","Run a config linter that calls new URL() on every careers_url/api field at load time.","Trim and strip BOM/non-breaking spaces from config values before use.","Keep URL templates out of string interpolation with possibly-undefined variables."],"tags":["url","validation","ssrf-guard","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"}