{"record":{"id":"554936554262f3e8","repo":"santifer/career-ops","slug":"rippling-cannot-derive-api-url-for-entry-name","errorCode":null,"errorMessage":"rippling: cannot derive API URL for ${entry.name}","messagePattern":"rippling: cannot derive API URL for (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/rippling.mjs","lineNumber":74,"sourceCode":"  if (parsed.protocol !== 'https:') throw new Error(`rippling: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== API_HOST) {\n    throw new Error(`rippling: untrusted hostname \"${parsed.hostname}\" — must be ${API_HOST}`);\n  }\n  return url;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'rippling',\n\n  detect(entry) {\n    const slug = resolveSlug(entry);\n    return slug ? { url: apiUrlForSlug(slug) } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const slug = resolveSlug(entry);\n    if (!slug) throw new Error(`rippling: cannot derive API URL for ${entry.name}`);\n    const apiUrl = apiUrlForSlug(slug);\n    assertRipplingApiUrl(apiUrl);\n    // redirect:'error' prevents SSRF via server-side redirects\n    const json = await ctx.fetchJson(apiUrl, { redirect: 'error' });\n    return parseRipplingResponse(json, entry.name);\n  },\n};\n\n/**\n * Parse a Rippling board API response. Exported for unit tests.\n *\n * The response is a top-level JSON ARRAY of postings. Field mapping → the\n * normalized Job shape:\n *   - title:    `name`, trimmed (postings without one are dropped).\n *   - url:      `url` — an absolute `https:` posting URL host-locked to\n *               `ats.rippling.com` (Rippling always serves postings there, so an\n *               off-host or non-https URL is untrusted and the posting is dropped).\n *               It is the dedup key and is display-only (written to the","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/rippling.mjs#L56-L92","documentation":"The rippling provider derives its API URL from a slug in the provider entry via resolveSlug. If no slug can be resolved for the entry, fetch throws this error naming the entry, rather than calling the API with an undefined target.","triggerScenarios":"Calling fetch with an entry whose slug is missing, an empty string, or otherwise falsy after resolveSlug's checks — e.g. { name: 'acme' } with no slug/ats identifier, or a slug key typo'd in config.","commonSituations":"A newly added portals.yml company entry missing the Rippling slug, a renamed config key after an edit, or a company that moved off Rippling so the stored slug no longer applies.","solutions":["Add/fix the slug field on the provider entry (the identifier Rippling uses in its board URL).","Derive the slug from the company's public Rippling jobs URL (the path segment) and store it in the entry.","Remove or re-point the entry if the company no longer uses Rippling ATS."],"exampleFix":"// before\n{ name: 'acme' }\n// after\n{ name: 'acme', slug: 'acme' } // https://api.rippling.com/...?slug=acme","handlingStrategy":"validation","validationCode":"if (typeof entry.slug !== 'string' || !entry.slug.trim()) {\n  throw new Error(`Entry ${entry.name} is missing a Rippling slug`);\n}","typeGuard":"const hasSlug = (e) => typeof e?.slug === 'string' && e.slug.trim().length > 0;","tryCatchPattern":"try { jobs = await provider.fetch(entry, ctx); } catch (e) { if (e.message.includes('cannot derive API URL')) { console.error(`Add slug to entry ${entry.name}`); return null; } throw e; }","preventionTips":["Validate required entry fields (slug) when loading provider config.","Copy the slug from the company's public Rippling board URL when adding entries.","Re-verify slugs when a company changes ATS."],"tags":["config","slug","provider","missing-field"],"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"}