{"record":{"id":"d700a701729c7c16","repo":"santifer/career-ops","slug":"pinpoint-cannot-derive-api-url-for-entry-name","errorCode":null,"errorMessage":"pinpoint: cannot derive API URL for ${entry.name}","messagePattern":"pinpoint: cannot derive API URL for (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/pinpoint.mjs","lineNumber":64,"sourceCode":"    return null;\n  }\n  if (parsed.protocol !== 'https:') return null;\n  if (!PINPOINT_HOST_RE.test(parsed.hostname)) return null;\n  return `https://${parsed.hostname}/postings.json`;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'pinpoint',\n\n  detect(entry) {\n    const apiUrl = resolveApiUrl(entry);\n    return apiUrl ? { url: apiUrl } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const apiUrl = resolveApiUrl(entry);\n    if (!apiUrl) throw new Error(`pinpoint: cannot derive API URL for ${entry.name}`);\n    assertPinpointUrl(apiUrl);\n    // redirect:'error' prevents SSRF via server-side redirects\n    const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });\n    return parsePinpointResponse(json, entry.name);\n  },\n};\n\n/**\n * Parse a Pinpoint /postings.json response. Exported for unit tests.\n *\n * Pinpoint returns:\n *   { data: [{ title, url, path, location: { name, city, province, ... }, ... }] }\n *\n * Field mapping → the normalized Job shape:\n *   - title:    `title`, trimmed.\n *   - url:      `url` — an absolute posting URL on the tenant's own\n *               `<slug>.pinpointhq.com` host. It is display-only (written to the\n *               pipeline and scan history, never server-fetched here), so it is","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/pinpoint.mjs#L46-L82","documentation":"The pinpoint provider only serves portal entries whose careers_url matches `https://<slug>.pinpointhq.com`. resolveApiUrl() returns null when the entry has no careers_url, an unparseable URL, a non-HTTPS URL, or a hostname that does not match the tenant regex. fetch() throws this error so the caller knows the provider cannot be used for this entry rather than silently returning no jobs.","triggerScenarios":"Calling fetch() with an entry whose careers_url is missing, empty, not a string, malformed (new URL() throws), using http: instead of https:, or on a non-pinpointhq.com hostname (e.g. a custom careers domain or a different ATS).","commonSituations":"A portals.yml entry lists a company that moved off Pinpoint (ATS migration) but still points at the old careers URL; a custom careers domain (careers.example.com) is used instead of the <slug>.pinpointhq.com subdomain; the URL was typoed or entered as http://.","solutions":["Set entry.careers_url to the actual tenant subdomain URL, e.g. https://<slug>.pinpointhq.com","Verify the company still uses Pinpoint by checking the careers page; if it migrated, change the provider entry accordingly","Ensure the URL is https: and parseable (no spaces, full scheme included)","If the tenant uses a custom domain, find the underlying <slug>.pinpointhq.com host and use that"],"exampleFix":"// before (portals.yml)\ncareers_url: careers.acme.com\n// after\ncareers_url: https://acme.pinpointhq.com","handlingStrategy":"validation","validationCode":"import { URL } from 'url';\nconst RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.pinpointhq\\.com$/;\nfunction resolvable(entry) {\n  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  if (!raw) return false;\n  try {\n    const u = new URL(raw);\n    return u.protocol === 'https:' && RE.test(u.hostname);\n  } catch { return false; }\n}","typeGuard":"function isPinpointEntry(entry) {\n  return typeof entry?.careers_url === 'string'\n    && /^https:\\/\\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.pinpointhq\\.com\\/?$/.test(entry.careers_url);\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('pinpoint: cannot derive API URL')) {\n    console.warn(`Skipping ${entry.name}: careers_url is not a Pinpoint tenant URL`);\n    return [];\n  }\n  throw err;\n}","preventionTips":["Store full https://<slug>.pinpointhq.com URLs in portals.yml, never bare hostnames or custom domains","Run a config linter that pattern-checks careers_url against the provider's host regex before scans","When a company migrates ATS, update the careers_url and provider in the same commit"],"tags":["config","url-validation","ssrf-protection"],"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"}