{"record":{"id":"4528aff245591b42","repo":"santifer/career-ops","slug":"justjoin-invalid-url-url","errorCode":null,"errorMessage":"justjoin: invalid URL: ${url}","messagePattern":"justjoin: invalid URL: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/justjoin.mjs","lineNumber":19,"sourceCode":"// @ts-check\n/** @typedef {import('./_types.js').Provider} Provider */\n\n// JustJoin.it provider — hits the current candidate offers API.\n// 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 {","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/justjoin.mjs#L1-L37","documentation":"Thrown by assertJustJoinUrl() when new URL(url) throws — the input is not a parseable URL at all (empty, malformed, missing scheme, unbalanced brackets). This is the first guard in the justjoin.it URL validator; it precedes the protocol, hostname, and path checks. The validator is the single chokepoint for both detect() and buildApiUrl().","triggerScenarios":"An api or careers_url field that is empty, contains whitespace only, lacks a scheme (e.g. 'justjoin.it/job-offers'), or contains characters that break the URL parser (spaces, stray brackets).","commonSituations":"A portals.yml entry with a blank or commented-out url; a copy-paste that lost the https:// prefix; a YAML quoting issue that produced an unexpected type.","solutions":["Set api or careers_url to a full https://justjoin.it/... URL.","Check the YAML entry for missing quotes or an empty value.","Verify the field is a non-empty string before it reaches the validator."],"exampleFix":"# before\nacme:\n  provider: justjoin\n  api: justjoin.it/api/candidate-api/offers\n\n# after\nacme:\n  provider: justjoin\n  api: https://justjoin.it/api/candidate-api/offers","handlingStrategy":"validation","validationCode":"function isParseableUrl(url) {\n  try { new URL(url); return true; } catch { return false; }\n}\n// before scan:\nfor (const e of justjoinEntries) {\n  const u = e.api || e.careers_url;\n  if (typeof u !== 'string' || !u.trim() || !isParseableUrl(u)) {\n    console.warn(`justjoin ${e.name}: api/careers_url is missing or unparseable`);\n  }\n}","typeGuard":"/** @param {string} url @returns {boolean} */\nfunction isParseableHttpsUrl(url) {\n  try { new URL(url); return true; } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Require a full https:// URL in the api/careers_url field for every justjoin entry.","Lint config entries for empty or scheme-less URLs before scanning.","Quote YAML string values to avoid type-coercion surprises."],"tags":["justjoin","url-validation","ssrf-guard","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}