{"record":{"id":"a5d25f84a0bea448","repo":"santifer/career-ops","slug":"comeet-cannot-derive-api-url-for-entry-name-s","errorCode":null,"errorMessage":"comeet: cannot derive API URL for ${entry.name} (set api: to the full careers-api positions URL)","messagePattern":"comeet: cannot derive API URL for (.+?) \\(set api: to the full careers-api positions URL\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/comeet.mjs","lineNumber":88,"sourceCode":"  const parsed = Date.parse(value);\n  return Number.isNaN(parsed) ? undefined : parsed;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'comeet',\n\n  detect(entry) {\n    const apiUrl = resolveApiUrl(entry);\n    // The DetectHit url is informational (the framework may log it), so strip\n    // the secret ?token= before returning it — fetch() re-resolves the real\n    // URL from the entry, so redaction here is safe.\n    return apiUrl ? { url: redactToken(apiUrl) } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const apiUrl = resolveApiUrl(entry);\n    if (!apiUrl) throw new Error(`comeet: cannot derive API URL for ${entry.name} (set api: to the full careers-api positions URL)`);\n    assertComeetUrl(apiUrl);\n    // redirect:'error' prevents SSRF via server-side redirects; combined with\n    // assertComeetUrl above it guarantees the final hostname stays www.comeet.co.\n    const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });\n    return parseComeetResponse(json, entry.name);\n  },\n};\n\n/**\n * Parse a Comeet careers-api positions response. Exported for unit tests.\n *\n * Comeet returns a top-level ARRAY of position objects:\n *   [{ name, location: { name, is_remote }, url_active_page,\n *      url_comeet_hosted_page, time_updated, ... }]\n *\n * - url: prefer `url_active_page` (the tenant's live careers page), fall back to\n *   `url_comeet_hosted_page` (the Comeet-hosted page). Both are public, display-\n *   only URLs (recorded in the pipeline/history, never server-fetched here), so","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/comeet.mjs#L70-L106","documentation":"The primary comeet misconfiguration error: resolveApiUrl(entry) returned null, meaning neither entry.api nor entry.careers_url is a full Comeet careers-api positions URL. Comeet's positions endpoint needs both a company uid and a per-tenant token, neither of which is derivable from a branded careers page, so the full https://www.comeet.co/careers-api/2.0/company/<uid>/positions?token=<token> URL must be supplied explicitly via the api: field. This is the error real users hit; the four assertComeetUrl errors (161-164) are pre-empted by it.","triggerScenarios":"entry.api is absent and entry.careers_url is the branded www.comeet.com/jobs/... page (no token, wrong host, wrong path); or entry.api is set but is not https / not on www.comeet.co / not under /careers-api/. isComeetApiUrl returns false for both fields, so resolveApiUrl returns null.","commonSituations":"Copying the public careers page URL into careers_url and expecting the provider to derive the API call (it cannot — the token is secret and not on that page); pasting an http or www.comeet.com URL into api:; forgetting to migrate an old entry after Comeet renamed the endpoint.","solutions":["Set entry.api to the full positions URL from Comeet's careers-API docs, including the ?token= query param: https://www.comeet.co/careers-api/2.0/company/<uid>/positions?token=<token>.","If you only have the branded page, obtain the careers-api URL from Comeet's admin/recruiter dashboard — the token is not scrapable from the public page.","Gate with provider.detect(entry) (returns null when resolveApiUrl does) so misconfigured entries are skipped instead of throwing."],"exampleFix":"# before — branded page, no token, will not resolve\n- name: Acme\n  provider: comeet\n  careers_url: https://www.comeet.com/jobs/acme\n\n# after — full careers-api positions URL with token\n- name: Acme\n  provider: comeet\n  api: https://www.comeet.co/careers-api/2.0/company/abc/positions?token=SECRET_TOKEN","handlingStrategy":"validation","validationCode":"// detect() returns null iff resolveApiUrl does — i.e. exactly when fetch() would throw 165.\nimport comeet from './providers/comeet.mjs';\nif (!comeet.detect(entry)) {\n  // entry.api is not a full https://www.comeet.co/careers-api/.../positions?token=... URL — set it\n}","typeGuard":"/** True when entry has a usable Comeet careers-api positions URL (with token). */\nfunction isComeetEntry(entry) {\n  const raw = typeof entry?.api === 'string' ? entry.api : '';\n  if (!raw) return false;\n  try {\n    const p = new URL(raw);\n    return p.protocol === 'https:' && p.hostname === 'www.comeet.co' && p.pathname.startsWith('/careers-api/') && p.searchParams.has('token');\n  } catch { return false; }\n}","tryCatchPattern":"try { await comeet.fetch(entry, ctx); }\ncatch (e) {\n  if (/^comeet: cannot derive API URL/.test(e.message)) {\n    // config issue — set entry.api to the full positions URL; do not retry\n  } else throw e;\n}","preventionTips":["Set entry.api to the full positions URL including ?token= — the branded page has no token.","Run detect() before fetch() to skip misconfigured entries.","The token is secret — store it in config, never log it (the provider redacts it on throw)."],"tags":["config","comeet","ats","url-resolution","token","ssrf"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}