{"record":{"id":"5cb62aa1bcde2be3","repo":"santifer/career-ops","slug":"jobstreet-url-must-use-https-url","errorCode":null,"errorMessage":"jobstreet: URL must use HTTPS: ${url}","messagePattern":"jobstreet: URL must use HTTPS: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobstreet.mjs","lineNumber":84,"sourceCode":"// 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 {\n    return 'https://id.jobstreet.com';\n  }","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/jobstreet.mjs#L66-L102","documentation":"The jobstreet provider's assertJobstreetUrl() validates every URL used against the SEEK v5 JobSearch API. If the configured `api:` URL (or any derived URL) parses but its protocol is not `https:`, it throws this error. The guard exists to prevent credentials/query params from being sent over plaintext and as part of the SSRF defense on the base URL.","triggerScenarios":"A portals.yml entry with `provider: jobstreet` sets `api: http://id.jobstreet.com/api/jobsearch/v5/search` (http instead of https), or code constructs the search endpoint from a base origin captured over plain HTTP.","commonSituations":"Copy-pasting a URL from an internal proxy or local dev environment; hand-writing the api URL and forgetting the 's'; an http:// link stored in an older portals.yml from a pre-HTTPS template.","solutions":["Change the URL scheme in portals.yml `api:` to https:// (e.g. https://id.jobstreet.com/api/jobsearch/v5/search).","If no `api:` is set, remove it and let the provider use its default https://id.jobstreet.com/api/jobsearch/v5/search.","Verify the hostname is also in the allowlist (jobstreet.com, jobstreet.co.id, sg/my/id subdomains, hk.jobsdb.com, www.seek.com.au, www.seek.co.nz) so the next check passes."],"exampleFix":"// before (portals.yml)\nprovider: jobstreet\napi: http://sg.jobstreet.com/api/jobsearch/v5/search\n// after\nprovider: jobstreet\napi: https://sg.jobstreet.com/api/jobsearch/v5/search","handlingStrategy":"validation","validationCode":"const u = new URL(entry.api);\nif (u.protocol !== 'https:') throw new Error(`api must be https: ${entry.api}`);","typeGuard":"const isHttpsUrl = (s) => { try { return new URL(s).protocol === 'https:'; } catch { return false; } };","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (String(e.message).startsWith('jobstreet: URL must use HTTPS')) {\n    entry.api = entry.api.replace(/^http:/, 'https:');\n  }\n}","preventionTips":["Always write full https:// URLs in portals.yml api: fields.","Add a config lint step that rejects http: URLs for provider endpoints.","Prefer omitting api: and using the provider default, which is already HTTPS."],"tags":["url-validation","https","ssrf-protection","config"],"backgroundTag":"invalid-url","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"}