{"record":{"id":"d96bbc82a308f876","repo":"santifer/career-ops","slug":"workable-invalid-url-url","errorCode":null,"errorMessage":"workable: invalid URL: ${url}","messagePattern":"workable: invalid URL: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/workable.mjs","lineNumber":114,"sourceCode":"  throw lastErr;\n}\n\n// Process-wide serialization: apply.workable.com fronts every tenant on the\n// same host, so this process never needs more than one in-flight request to\n// it at a time.\nlet workableQueue = Promise.resolve();\nfunction serialized(fn) {\n  const result = workableQueue.then(fn, fn);\n  workableQueue = result.then(() => undefined, () => undefined);\n  return result;\n}\n\nfunction assertWorkableUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`workable: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`workable: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_WORKABLE_HOSTS.has(parsed.hostname)) {\n    throw new Error(`workable: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_WORKABLE_HOSTS].join(', ')}`);\n  }\n  return url;\n}\n\n/**\n * Extract the account slug from a tracked_companies entry's careers_url.\n * @returns {string|null}\n */\nexport function resolveWorkableSlug(entry) {\n  const raw = entry && typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  if (!raw) return null;\n  let parsed;\n  try {\n    parsed = new URL(raw);","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/workable.mjs#L96-L132","documentation":"assertWorkableUrl pins Workable requests to apply.workable.com (ALLOWED_WORKABLE_HOSTS). This branch fires when new URL(url) throws on a URL built from widgetUrlFor(slug) or feedUrlFor(slug). Because the slug is first validated by SLUG_RE (^[A-Za-z0-9][A-Za-z0-9_-]*$), the only realistic way to hit this is a maintainer breaking the URL templates — not a user-config error.","triggerScenarios":"A maintainer edits widgetUrlFor/feedUrlFor and drops the scheme or otherwise makes it unparseable; a bad merge. The slug is sanitized before interpolation, so crafted careers_url values cannot reach here.","commonSituations":"A refactor of the URL templates; running a patched fork.","solutions":["Restore the widget template to https://apply.workable.com/api/v1/widget/accounts/${slug}?details=true.","Unit-test assertWorkableUrl(widgetUrlFor(\"acme\"))."],"exampleFix":"// before\nconst widgetUrlFor = (slug) => `apply.workable.com/api/v1/widget/accounts/${slug}`;\n// after\nconst widgetUrlFor = (slug) => `https://apply.workable.com/api/v1/widget/accounts/${slug}?details=true`;","handlingStrategy":"validation","validationCode":"for (const tpl of [widgetUrlFor, feedUrlFor]) {\n  try { new URL(tpl(\"acme\")); }\n  catch { throw new Error(\"workable: URL template produced an unparseable URL\"); }\n}","typeGuard":"const isAbsoluteUrl = (s) => { try { new URL(s); return true; } catch { return false; } };","tryCatchPattern":null,"preventionTips":["Validate the slug via SLUG_RE before building any URL (already done).","Keep the URL templates as module constants covered by a unit test."],"tags":["validation","url","ssrf-guard","workable","invariant"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}