{"record":{"id":"4874b61f3b9be7ff","repo":"santifer/career-ops","slug":"justjoin-url-path-must-be-job-offers-or-api-can","errorCode":null,"errorMessage":"justjoin: URL path must be /job-offers or /api/candidate-api/offers: ${url}","messagePattern":"justjoin: URL path must be /job-offers or /api/candidate-api/offers: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/justjoin.mjs","lineNumber":26,"sourceCode":"const 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\nfunction normalizeLocation(offer) {\n  const parts = [];\n  const workplace = String(offer?.workplaceType || '').trim();","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/justjoin.mjs#L8-L44","documentation":"Thrown by assertJustJoinUrl() when the URL is valid HTTPS on justjoin.it but the pathname is neither /job-offers (or a prefix of it) nor the exact /api/candidate-api/offers endpoint. The provider restricts fetchable paths to the two known justjoin.it routes so a tampered URL cannot reach an arbitrary path on the host.","triggerScenarios":"An api URL pointing at a different justjoin.it endpoint (e.g. an old /api/v1/... path); a careers_url whose path is neither /job-offers nor the API; a typo or stale endpoint from a prior justjoin.it API version.","commonSituations":"justjoin.it changed its API path (version bump); the user configured a careers_url to the company landing page rather than /job-offers; an old endpoint was retired.","solutions":["For the api field, use https://justjoin.it/api/candidate-api/offers (or omit it — buildApiUrl defaults to API_BASE).","For careers_url, use a https://justjoin.it/job-offers/... URL.","If justjoin.it renamed the endpoint, update API_BASE and the path allow-check in providers/justjoin.mjs."],"exampleFix":"# before — stale endpoint\nacme:\n  provider: justjoin\n  api: https://justjoin.it/api/v1/offers\n\n# after — current endpoint\nacme:\n  provider: justjoin\n  api: https://justjoin.it/api/candidate-api/offers","handlingStrategy":"validation","validationCode":"function isAllowedJustJoinPath(url) {\n  try {\n    const p = new URL(url);\n    return p.pathname.startsWith('/job-offers') || p.pathname === '/api/candidate-api/offers';\n  } catch { return false; }\n}","typeGuard":"/** @param {string} url @returns {boolean} */\nfunction isJustJoinApiUrl(url) {\n  try {\n    const p = new URL(url);\n    return p.protocol === 'https:' && p.hostname === 'justjoin.it'\n      && (p.pathname.startsWith('/job-offers') || p.pathname === '/api/candidate-api/offers');\n  } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Prefer omitting api and letting buildApiUrl default to API_BASE to avoid stale endpoints.","Track justjoin.it API version changes in a changelog and update API_BASE promptly.","Lint api fields against the allowed-path set."],"tags":["justjoin","url-validation","api-version","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}