{"record":{"id":"fa4d28b05f4b0c5a","repo":"santifer/career-ops","slug":"consider-entry-name-needs-an-https-careers-url","errorCode":null,"errorMessage":"consider: ${entry.name} needs an https careers_url on a public host","messagePattern":"consider: (.+?) needs an https careers_url on a public host","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/consider.mjs","lineNumber":92,"sourceCode":"  }\n  if (Array.isArray(job.normalizedLocations) && job.normalizedLocations.length) {\n    return job.normalizedLocations.map(l => l?.label || l?.value).filter(Boolean).join(', ');\n  }\n  return job.remote ? 'Remote' : '';\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'consider',\n\n  detect(entry) {\n    const origin = resolveOrigin(entry);\n    return entry.consider_board && origin ? { url: origin + ENDPOINT_PATH } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const origin = resolveOrigin(entry);\n    if (!origin) throw new Error(`consider: ${entry.name} needs an https careers_url on a public host`);\n    if (!entry.consider_board) throw new Error(`consider: ${entry.name} needs a 'consider_board' id in portals.yml`);\n    const size = Number.isInteger(entry.consider_size) && entry.consider_size > 0 ? entry.consider_size : DEFAULT_SIZE;\n\n    const json = await ctx.fetchJson(origin + ENDPOINT_PATH, {\n      method: 'POST',\n      // redirect:'error' so a 3xx from the (config-driven) board host can't be\n      // followed to a private/metadata IP — the host guard above pins the first hop.\n      redirect: 'error',\n      headers: { 'content-type': 'application/json', accept: 'application/json', referer: origin + '/jobs' },\n      body: JSON.stringify({\n        meta: { size },\n        board: { id: String(entry.consider_board), isParent: true },\n        query: { promoteFeatured: true },\n      }),\n    });\n\n    const jobs = Array.isArray(json?.jobs) ? json.jobs : [];\n    return jobs","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/consider.mjs#L74-L110","documentation":"Thrown by the consider provider's fetch() when resolveOrigin(entry) returns null. resolveOrigin parses entry.careers_url and requires https, plus a public, non-internal hostname: it rejects IP literals (IPv4/IPv6), localhost, .local/.internal suffixes, and single-label hosts. Consider boards are always real registrable domains (jobs.founderful.com, etc.), so the guard both validates config and prevents an SSRF via a crafted careers_url aiming the POST at a private/metadata host.","triggerScenarios":"entry.careers_url is missing, unparseable, non-https, an IP literal, 'localhost', a .local/.internal host, or a single-label (no dot) host. Note this is checked before consider_board, so a missing consider_board surfaces as a different error (line 93) once the origin resolves.","commonSituations":"Omitting careers_url; using an http:// URL; pointing at an internal staging host (*.internal) that the SSRF guard intentionally blocks; a malformed URL pasted from a rich-text source.","solutions":["Set careers_url to the board's public https URL, e.g. https://jobs.founderful.com/jobs.","If you need a staging board, host it on a real public domain — the guard deliberately rejects internal hosts.","Gate with provider.detect(entry) (returns null when consider_board is missing OR origin fails) before calling fetch()."],"exampleFix":"# before — internal host, rejected by SSRF guard\n- name: Founderful\n  provider: consider\n  consider_board: wingman\n  careers_url: https://founderful.internal/jobs\n\n# after — public https host\n- name: Founderful\n  provider: consider\n  consider_board: wingman\n  careers_url: https://jobs.founderful.com/jobs","handlingStrategy":"validation","validationCode":"import consider from './providers/consider.mjs';\n// detect() requires BOTH a public https origin AND a consider_board id.\nif (!consider.detect(entry)) {\n  // origin failed (this error) OR consider_board missing — check careers_url first\n}","typeGuard":"/** True when careers_url is a public https host (not IP/localhost/internal). */\nfunction hasPublicHttpsOrigin(entry) {\n  const raw = typeof entry?.careers_url === 'string' ? entry.careers_url : '';\n  if (!raw) return false;\n  let host;\n  try { host = new URL(raw).hostname.toLowerCase(); } catch { return false; }\n  if (new URL(raw).protocol !== 'https:') return false;\n  if (host.endsWith('.')) host = host.slice(0, -1);\n  if (host.startsWith('[') || host.includes(':')) return false;\n  if (/^\\d{1,3}(\\.\\d{1,3}){3}$/.test(host)) return false;\n  if (host === 'localhost' || host === 'localhost.localdomain') return false;\n  if (host.endsWith('.local') || host.endsWith('.internal')) return false;\n  return host.includes('.');\n}","tryCatchPattern":"try { await consider.fetch(entry, ctx); }\ncatch (e) {\n  if (/^consider: .* needs an https careers_url/.test(e.message)) {\n    // SSRF/config guard — fix careers_url to a public https host; do not retry as-is\n  } else throw e;\n}","preventionTips":["Use the board's real public domain (jobs.founderful.com), never an internal host.","The SSRF guard intentionally rejects IP literals, localhost, and .local/.internal — do not try to bypass it.","Run detect() before fetch()."],"tags":["config","consider","ats","url-resolution","ssrf","hostname-guard"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}