{"record":{"id":"902e859b882770fb","repo":"santifer/career-ops","slug":"justjoin-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"justjoin: untrusted hostname \"${parsed.hostname}\" — must be justjoin.it","messagePattern":"justjoin: untrusted hostname \"(.+?)\" — must be justjoin\\.it","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/justjoin.mjs","lineNumber":23,"sourceCode":"// Browser URLs under https://justjoin.it/job-offers/... are accepted for\n// detection, but fetches use https://justjoin.it/api/candidate-api/offers.\n\nconst ALLOWED_HOSTS = new Set(['justjoin.it']);\nconst API_BASE = 'https://justjoin.it/api/candidate-api/offers';\nconst JOB_BASE = 'https://justjoin.it/job-offer/';\nconst PAGE_SIZE = 100;\nconst MAX_PAGES = 50;\n\nfunction assertJustJoinUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`justjoin: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`justjoin: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_HOSTS.has(parsed.hostname)) {\n    throw new Error(`justjoin: untrusted hostname \"${parsed.hostname}\" — must be justjoin.it`);\n  }\n  if (!parsed.pathname.startsWith('/job-offers') && parsed.pathname !== '/api/candidate-api/offers') {\n    throw new Error(`justjoin: URL path must be /job-offers or /api/candidate-api/offers: ${url}`);\n  }\n  return parsed;\n}\n\nfunction detectUrl(entry) {\n  const url = entry.api || entry.careers_url || '';\n  if (typeof url !== 'string' || !url.trim()) return null;\n  try {\n    const parsed = assertJustJoinUrl(url);\n    return { url: parsed.href };\n  } catch {\n    return null;\n  }\n}\n","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/justjoin.mjs#L5-L41","documentation":"Thrown by assertJustJoinUrl when the URL's hostname is not in the allow-list, which contains only 'justjoin.it'. The provider is a purpose-built scanner for one ATS, so it rejects any other host to prevent SSRF-style misuse or accidentally hitting a lookalike/mirror domain. Even a valid HTTPS URL to another job board is refused here.","triggerScenarios":"entry.api or entry.careers_url points at a hostname other than justjoin.it — e.g. 'https://justjoin.it.evil.example.com/api/candidate-api/offers', 'https://www.justjoin.it/...' (www subdomain), 'https://nofluffjobs.com/...'. Triggered via assertJustJoinUrl through buildApiUrl during fetch, or surfaced indirectly when detect() silently returns null and fetch() raises error 319.","commonSituations":"Copy-pasting a URL with a 'www.' prefix; typo-squat or redirect-tracked URLs (utm-wrapped links through a shortener host); configuring this provider against a different job board by mistake; a regional mirror domain.","solutions":["Set careers_url/api to a hostname of exactly 'justjoin.it' — drop 'www.' and any suffix","If you meant another job board, use that board's provider instead of justjoin","Strip redirect/shortener wrappers and use the canonical justjoin.it URL"],"exampleFix":"// before (portals.yml)\napi: https://www.justjoin.it/api/candidate-api/offers\n// after\napi: https://justjoin.it/api/candidate-api/offers","handlingStrategy":"validation","validationCode":"function isTrustedJustJoinUrl(url) {\n  try { return new URL(url).hostname === 'justjoin.it'; } catch { return false; }\n}\nif (!isTrustedJustJoinUrl(entry.careers_url || entry.api || '')) throw new Error('host must be justjoin.it');","typeGuard":"const isJustJoinHost = (u) => { try { return new URL(u).hostname === 'justjoin.it'; } catch { return false; } };","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('untrusted hostname')) {\n    console.error(`justjoin provider got a non-justjoin.it host: ${e.message}`);\n  } else throw e;\n}","preventionTips":["Use the bare hostname justjoin.it — never www., subdomains, or mirror domains","Run provider.detect(entry) before fetch and treat null as 'wrong provider', not a soft failure","Strip shortener/redirect-tracked links down to their canonical destination before configuring"],"tags":["validation","url","security","allowlist"],"backgroundTag":"invalid-url","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"}