santifer/career-ops · error · Error

breezy: invalid URL: ${url}

Error message

breezy: invalid URL: ${url}

What it means

Breezy validates URLs via `assertBreezyUrl`. The 'invalid URL' variant fires when `new URL(url)` throws — the input is not parseable as an absolute URL. First of three sequential SSRF guards.

Source

Thrown at providers/breezy.mjs:24

// Per-tenant subdomains are the variable part, so SSRF defence uses a regex
// match on `<safe-tenant>.breezy.hr` rather than a static allowlist (same
// approach as the recruitee / bamboohr providers).
//
// Breezy boards expose every published position as a public JSON array at
// `<tenant>.breezy.hr/json` — title, absolute url, location, and a published
// date, all in the list payload at zero token cost (no per-job request, so the
// scanner stays zero-token). Breezy's authenticated REST API (api.breezy.hr) is
// intentionally NOT used; only the public board feed.

const BREEZY_HOST_RE = /^[a-z0-9][a-z0-9-]*\.breezy\.hr$/;

/** @param {string} url */
function assertBreezyUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`breezy: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`breezy: URL must use HTTPS: ${url}`);
  if (!BREEZY_HOST_RE.test(parsed.hostname)) {
    throw new Error(`breezy: untrusted hostname "${parsed.hostname}" — must match <tenant>.breezy.hr`);
  }
  return url;
}

/**
 * Resolve the tenant origin (`https://<tenant>.breezy.hr`) from an entry.
 * Honours an explicit `api:` URL, else parses `careers_url`.
 * @param {import('./_types.js').PortalEntry} entry
 * @returns {string | null}
 */
function resolveOrigin(entry) {
  const rawApi = typeof entry.api === 'string' ? entry.api : '';
  const rawCareers = typeof entry.careers_url === 'string' ? entry.careers_url : '';
  const raw = (rawApi || rawCareers).trim();

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Provide a full `https://<tenant>.breezy.hr` URL.
  2. Validate entry URLs at config load to surface the offending row.
  3. Strip whitespace from templated URL fields.

Example fix

# before
- name: Acme
  api: acme.breezy.hr

# after
- name: Acme
  api: https://acme.breezy.hr
Defensive patterns

Strategy: validation

Validate before calling

function isValidAbsoluteUrl(u) {
  try { new URL(u); return true; } catch { return false; }
}
for (const f of ['api', 'careers_url']) {
  if (entry[f] && !isValidAbsoluteUrl(entry[f])) {
    throw new Error(`breezy: entry ${entry.name} has malformed ${f}: ${entry[f]}`);
  }
}

Prevention

When it happens

Trigger: `assertBreezyUrl(url)` (called on the derived `${origin}` or an `api:` field) receives a value `new URL()` cannot parse: schemeless host, protocol-relative URL, empty string, or illegal characters.

Common situations: An `api:` field set without `https://`, a careers_url broken by templating, or config injection mangling the scheme.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/e572f9f608fc93af. Report an issue: GitHub.