{"record":{"id":"daf81efe20054a79","repo":"santifer/career-ops","slug":"torre-untrusted-hostname-parsed-hostname-m","errorCode":null,"errorMessage":"torre: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_API_HOST}","messagePattern":"torre: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/torre.mjs","lineNumber":94,"sourceCode":"  'potential-to-develop',\n  '1-plus-year',\n  '2-plus-years',\n  '3-plus-years',\n  '5-plus-years',\n]);\nconst DEFAULT_EXPERIENCE = '1-plus-year';\n\n/** @param {string} url */\nfunction assertTorreUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`torre: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`torre: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_API_HOST) {\n    throw new Error(`torre: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_API_HOST}`);\n  }\n  return url;\n}\n\n/**\n * Build the search body from the portal entry. Only filters proven to affect\n * `total` are emitted — see the header note. Exported for tests.\n *\n * @param {any} entry\n * @returns {object}\n */\nexport function buildTorreQuery(entry) {\n  /** @type {Record<string, unknown>} */\n  const body = {};\n\n  const search = typeof entry?.search === 'string' ? entry.search.trim() : '';\n  if (search) {\n    // `experience` is mandatory here — omitting it is a hard 500, so it is","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/providers/torre.mjs#L76-L112","documentation":"After passing scheme checks, assertTorreUrl enforces a single trusted API host (TRUSTED_API_HOST). This error is thrown when the hostname differs — including lookalike/subdomain variants. Like its Muse counterpart, this is the SSRF/pin allowlist that stops fetches to attacker-controlled or redirected hosts.","triggerScenarios":"Calling assertTorreUrl with a URL on a different host or subdomain than the pinned Torre API host — e.g. 'www.torre.ai' when only 'torre.ai' is trusted, 'api.torre.co' (old domain), or 'torre.ai.evil.io'.","commonSituations":"Torre domain migration (torre.co → torre.ai style) leaving old URLs in config; adding/omitting 'www.'; third-party mirrors or job aggregators linking to copies of Torre postings; malicious URLs from untrusted input.","solutions":["Correct the hostname to exactly the pinned TRUSTED_API_HOST (check the constant at the top of providers/torre.mjs).","Strip redirect/tracking wrappers to recover the genuine Torre API URL before validating.","If Torre changed its API host officially, verify and update TRUSTED_API_HOST in providers/torre.mjs — do not loosen the check to a suffix match.","Route URLs for other hosts to their proper providers instead of forcing them through the Torre provider."],"exampleFix":"// before (provider pinned to api.torre.ai)\nassertTorreUrl('https://torre.co/api/v2');\n// Error: untrusted hostname \"torre.co\" — must be api.torre.ai\n// after\nassertTorreUrl('https://api.torre.ai/api/v2');","handlingStrategy":"validation","validationCode":"const TRUSTED_API_HOST = 'api.torre.ai'; // match the constant in providers/torre.mjs\nfunction isTrustedTorreUrl(url) {\n  try {\n    const u = new URL(url);\n    return u.protocol === 'https:' && u.hostname === TRUSTED_API_HOST;\n  } catch { return false; }\n}","typeGuard":"function isTorreApiUrl(v) {\n  if (typeof v !== 'string') return false;\n  try {\n    const u = new URL(v);\n    return u.protocol === 'https:' && u.hostname === 'api.torre.ai';\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  assertTorreUrl(url);\n} catch (err) {\n  if (String(err.message).includes('untrusted hostname')) {\n    console.error(`Rejecting non-Torre host (possible SSRF/mirror): ${err.message}`);\n    return null; // skip this URL\n  } else throw err;\n}","preventionTips":["Pin the exact host (subdomain included) in config; check the constant in providers/torre.mjs if Torre migrates domains.","Treat URLs from aggregators/emails as untrusted — resolve redirects and re-validate.","Never weaken the host check to includes()/suffix matching.","Route each job board's URLs to its own provider rather than forcing one validator."],"tags":["ssrf","url-validation","security","config"],"backgroundTag":"untrusted-hostname","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}