{"record":{"id":"bf6bdebc2c61dd9f","repo":"santifer/career-ops","slug":"jobstreet-invalid-url-url","errorCode":null,"errorMessage":"jobstreet: invalid URL: ${url}","messagePattern":"jobstreet: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobstreet.mjs","lineNumber":82,"sourceCode":"// — my/sg.jobstreet.com, www.seek.com.au, www.seek.co.nz — serves /job/<id> and\n// answers 404 on /id/job/<id> (verified against live ids, 2026-08-28). A global\n// switch either way breaks one market, which is why this is keyed on the host.\nconst ID_LOCALE_HOSTS = new Set(['id.jobstreet.com', 'www.jobstreet.co.id', 'jobstreet.co.id']);\n\n/** @param {string} origin — scheme + hostname */\nfunction jobDetailPath(origin) {\n  let host = '';\n  try { host = new URL(origin).hostname; } catch { /* fall through to the common path */ }\n  return ID_LOCALE_HOSTS.has(host) ? '/id/job/' : '/job/';\n}\n\n/** @param {string} url */\nfunction assertJobstreetUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`jobstreet: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`jobstreet: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_JOBSTREET_HOSTS.has(parsed.hostname))\n    throw new Error(`jobstreet: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_JOBSTREET_HOSTS].join(', ')}`);\n  return url;\n}\n\n/**\n * Derive the origin from the API hostname.\n * e.g. id.jobstreet.com → https://id.jobstreet.com\n * @param {string} apiUrl\n * @returns {string}\n */\nfunction deriveOrigin(apiUrl) {\n  try {\n    const parsed = new URL(apiUrl);\n    return `${parsed.protocol}//${parsed.hostname}`;\n  } catch {","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/jobstreet.mjs#L64-L100","documentation":"assertJobstreetUrl validates URLs used by the Jobstreet/SEEK provider. It throws 'invalid URL' when new URL(url) throws, meaning the input is not a well-formed absolute URL. This is the first of three gates (parse → https → host allowlist) protecting the server-side fetch from malformed config and SSRF.","triggerScenarios":"Calling assertJobstreetUrl with '', 'id.jobstreet.com/api/jobsearch/v5/search' (no scheme), 'https://', or any other string the URL constructor rejects.","commonSituations":"The api: field in a portals.yml jobstreet entry written without https://; env-var placeholders left unsubstituted; trailing whitespace/newlines from YAML editing; relative endpoint paths configured instead of absolute URLs.","solutions":["Set the api field to a full absolute URL, e.g. https://id.jobstreet.com/api/jobsearch/v5/search","Trim/inspect the raw config value for stray whitespace or placeholder text","Verify with new URL(value) in a Node REPL","Prefer omitting api entirely and letting the provider use DEFAULT_API"],"exampleFix":"// before (portals.yml)\napi: id.jobstreet.com/api/jobsearch/v5/search\n// after\napi: https://id.jobstreet.com/api/jobsearch/v5/search","handlingStrategy":"validation","validationCode":"function isValidJobstreetUrl(url) {\n  try {\n    const p = new URL(url);\n    const allowed = ['id.jobstreet.com','www.jobstreet.com','www.jobstreet.co.id','jobstreet.com','jobstreet.co.id','sg.jobstreet.com','my.jobstreet.com','hk.jobsdb.com','www.seek.com.au','www.seek.co.nz'];\n    return p.protocol === 'https:' && allowed.includes(p.hostname);\n  } catch { return false; }\n}","typeGuard":"function isParseableAbsoluteUrl(v) {\n  return typeof v === 'string' && URL.canParse(v);\n}","tryCatchPattern":"try {\n  assertJobstreetUrl(cfg.api);\n} catch (e) {\n  if (/invalid URL/.test(e.message)) {\n    console.error(`jobstreet api must be an absolute https:// URL, got: ${JSON.stringify(cfg.api)}`);\n    return [];\n  }\n  throw e;\n}","preventionTips":["Omit the api field to use the provider's DEFAULT_API instead of hand-writing URLs","Always include the https:// scheme in configured endpoints","Trim YAML values; a trailing newline can make URL parsing fail","Pick host + siteKey pairs consistently (e.g. hk.jobsdb.com with HK-Main) and validate with the allowlist in mind"],"tags":["url-validation","config","ssrf-protection","nodejs"],"backgroundTag":"invalid-url-format","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}