{"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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/pinpoint.mjs#L46-L82","documentation":"fetch() calls resolveApiUrl(entry) and throws when it returns null. resolveApiUrl returns null if entry.careers_url is missing, not a string, unparseable by the URL constructor, not HTTPS, or its hostname fails PINPOINT_HOST_RE. Unlike the assert* guards (which throw on a URL you already have), this fires during fetch when the entry itself lacks the data needed to build the API URL at all.","triggerScenarios":"entry.careers_url is undefined/null/empty; entry.careers_url is a non-HTTPS URL (http://acme.pinpointhq.com); entry.careers_url hostname is a branded domain that is not *.pinpointhq.com; entry.careers_url is a malformed string that throws inside new URL().","commonSituations":"A job_boards row was created without a careers_url (detect() returned null but fetch was still invoked directly); a YAML/JSON config field was misnamed (career_url vs careers_url); the entry was constructed programmatically and the URL field was dropped by a serializer.","solutions":["Ensure the entry passed to fetch has a valid string careers_url that is https://<slug>.pinpointhq.com.","If you are calling fetch() directly, call detect(entry) first and skip when it returns null — detect uses the same resolveApiUrl and returns null cleanly without throwing.","Check the config source (e.g. portals.yml) for a typo'd or missing careers_url field for this entry name.","If the board genuinely has no pinpointhq.com URL, do not use the pinpoint provider for this entry."],"exampleFix":"// before — fetch called on an entry detect() would reject\nawait provider.fetch({ name: 'Acme' }, ctx);\n// after — guard with detect()\nif (provider.detect(entry)) {\n  await provider.fetch(entry, ctx);\n}","handlingStrategy":"validation","validationCode":"// detect() uses the same resolveApiUrl and returns null instead of throwing\nconst detected = provider.detect(entry);\nif (!detected) {\n  console.warn(`skip ${entry.name}: pinpoint cannot derive API URL`);\n  continue;\n}\nawait provider.fetch(entry, ctx);","typeGuard":"null","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (/cannot derive API URL/.test(e.message)) {\n    console.warn(`[skip] ${entry.name}: no pinpointhq.com careers_url — fix config`);\n  } else throw e;\n}","preventionTips":["Gate every fetch() call on detect() returning non-null.","Validate at config-load time that pinpoint entries have a string careers_url matching the host regex.","Distinguish 'cannot derive' (config/missing data) from assert failures (bad URL) in error handling."],"tags":["config","provider","pinpoint","missing-url","resolution"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}