{"record":{"id":"b042478848b2da55","repo":"santifer/career-ops","slug":"h1b-api-base-must-not-embed-credentials","errorCode":null,"errorMessage":"H1B_API_BASE must not embed credentials.","messagePattern":"H1B_API_BASE must not embed credentials\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/h1b-sponsor/lib/api.mjs","lineNumber":55,"sourceCode":"  // (an unset shell variable, an empty .env line, a CI secret that did not\n  // populate), and silently falling back would send someone's shortlist and\n  // their token to a host they believed they had replaced.\n  if (raw === undefined) return DEFAULT_BASE;\n  const trimmed = String(raw).trim();\n  if (!trimmed) {\n    throw new Error('H1B_API_BASE is set but empty. Unset it to use the default endpoint.');\n  }\n\n  let parsed;\n  try {\n    parsed = new URL(trimmed);\n  } catch {\n    throw new Error(`H1B_API_BASE is not a valid URL: ${trimmed}`);\n  }\n  if (parsed.username || parsed.password) {\n    // Undici refuses a credentialed Request anyway, and the value reaches\n    // stdout through the source field, so this would print a password.\n    throw new Error('H1B_API_BASE must not embed credentials.');\n  }\n  if (parsed.search || parsed.hash) {\n    // Paths are appended, so a query or fragment swallows them: the request\n    // would go to the base itself and answer about a company never asked for.\n    throw new Error('H1B_API_BASE must not contain a query string or a fragment.');\n  }\n  // Plain http would put an Authorization header on the wire in the clear.\n  // Loopback is exempt so a self-hoster can develop against a local worker,\n  // but only for http. Exempting every scheme on a loopback host let\n  // ftp://localhost and ws://localhost past validation, and those die later\n  // inside fetch as a bare \"fetch failed\", which is the opaque failure this\n  // check exists to replace with a named configuration error.\n  const loopback = /^(localhost|127\\.\\d+\\.\\d+\\.\\d+|\\[::1\\]|::1)$/i.test(parsed.hostname);\n  const allowedScheme = parsed.protocol === 'https:' || (parsed.protocol === 'http:' && loopback);\n  if (!allowedScheme) {\n    throw new Error(`H1B_API_BASE must use https, or http on loopback: ${trimmed}`);\n  }\n  // Appended as `${base}/employers/...`, so a trailing slash would double up.","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/plugins/h1b-sponsor/lib/api.mjs#L37-L73","documentation":"resolveBase() rejects an H1B_API_BASE that embeds userinfo credentials (user:password@ in the URL). Undici refuses credentialed Requests anyway, and the URL is echoed to stdout via the source field, so credentials would be printed. The library throws a named error instead of leaking a secret.","triggerScenarios":"Setting H1B_API_BASE to something like https://user:token@h1b.example.com and calling any plugin API function that builds a request from apiBase().","commonSituations":"Putting an API token into the URL as basic-auth credentials because a cURL example used -u or user:pass@host syntax; copying an authenticated URL from a browser or proxy tool.","solutions":["Remove the user:password@ portion from H1B_API_BASE and keep only scheme+host+path.","Pass credentials out-of-band if the backend requires them — e.g. an Authorization header or a token env var consumed by your proxy — not in the URL.","If the API is behind basic auth, front it with a local reverse proxy that injects the header, and point H1B_API_BASE at the proxy."],"exampleFix":"// before (.env)\nH1B_API_BASE=https://admin:s3cret@h1b.example.com\n// after\nH1B_API_BASE=https://h1b.example.com","handlingStrategy":"validation","validationCode":"function hasEmbeddedCredentials(v) {\n  try { const u = new URL(v); return Boolean(u.username || u.password); }\n  catch { return false; }\n}\n// assert before running: if (hasEmbeddedCredentials(process.env.H1B_API_BASE)) throw ...","typeGuard":"function isCredentialFreeUrl(v) {\n  try { const u = new URL(v); return !u.username && !u.password; } catch { return false; }\n}","tryCatchPattern":"try {\n  await lookupEmployer(name);\n} catch (e) {\n  if (e.message === 'H1B_API_BASE must not embed credentials.') {\n    console.error('Remove user:pass@ from H1B_API_BASE; pass secrets via headers/proxy instead.');\n    return;\n  }\n  throw e;\n}","preventionTips":["Never put tokens in URLs — cURL -u syntax does not translate to a base-URL config.","Grep .env for '@' inside H1B_API_BASE as part of pre-commit checks.","Log the redacted origin+path of the base at startup so credentials would be visible immediately.","If the upstream needs auth, terminate it at a local proxy that injects headers."],"tags":["configuration","security","url-validation"],"backgroundTag":"credentials-in-url","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}