{"record":{"id":"667aef85a0ec3610","repo":"JuliusBrussee/caveman","slug":"private-device-authorization-omitted-its-browser-url","errorCode":null,"errorMessage":"private device authorization omitted its browser URL","messagePattern":"private device authorization omitted its browser URL","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":9605,"sourceCode":"    throw new Error(\"--instance requires a private HTTPS origin (HTTP loopback is allowed for local development)\");\n  }\n  return { noBrowser, instance: url.origin };\n}\n\nfunction secureLoginURL(url: URL, allowLoopback = true): boolean {\n  return !url.username && !url.password && (url.protocol === \"https:\" ||\n    (allowLoopback && url.protocol === \"http:\" && [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname)));\n}\n\nfunction privateVerificationURL(code: Record<string, unknown>, instance: string): string {\n  if (typeof code.device_code !== \"string\" || !code.device_code || code.device_code.length > 4096 ||\n      typeof code.user_code !== \"string\" || !/^[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/.test(code.user_code) ||\n      typeof code.expires_in !== \"number\" || !Number.isFinite(code.expires_in) || code.expires_in <= 0 || code.expires_in > 3600 ||\n      (code.interval !== undefined && (typeof code.interval !== \"number\" || !Number.isFinite(code.interval) || code.interval < 0 || code.interval > 60))) {\n    throw new Error(\"private device authorization returned an invalid code response\");\n  }\n  const value = code.verification_uri_complete ?? code.verification_uri;\n  if (typeof value !== \"string\") throw new Error(\"private device authorization omitted its browser URL\");\n  const url = new URL(value);\n  if (!secureLoginURL(url, new URL(instance).protocol === \"http:\") || url.hash) {\n    throw new Error(\"private device authorization returned an unsafe browser URL\");\n  }\n  url.searchParams.set(\"user_code\", code.user_code);\n  url.searchParams.set(\"connection\", \"mcp\");\n  url.searchParams.set(\"client_name\", \"Caveman CLI\");\n  return url.href;\n}\n\nfunction openLoginBrowser(url: string): void {\n  const opener = loginBrowserOpener(url);\n  if (!which(opener.command)) {\n    process.stderr.write(`  browser opener unavailable; open ${url}\\n`);\n    return;\n  }\n  const child = spawn(opener.command, opener.args, { detached: true, stdio: \"ignore\", windowsHide: true });\n  child.once(\"error\", () => process.stderr.write(`  browser did not open; open ${url}\\n`));","sourceCodeStart":9587,"sourceCodeEnd":9623,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L9587-L9623","documentation":"After the device-code response passes field validation, Caveman needs a URL to open in the user's browser: it reads verification_uri_complete, falling back to verification_uri. If neither field is present as a string in the private instance's device authorization response, there is nothing to open, so the CLI throws this error instead of attempting to launch a browser with undefined.","triggerScenarios":"The device authorization endpoint of the private instance returns a JSON body that lacks both verification_uri_complete and verification_uri (or provides them as non-strings, e.g. numbers or null), while the rest of the payload passed the earlier device_code/user_code/expires_in checks.","commonSituations":"A minimal or hand-rolled OAuth device-flow implementation that omits verification_uri; an authorization server that only returns verification_uri when a client option is enabled; a proxy stripping unknown fields from the response.","solutions":["Configure the private instance's authorization server to include verification_uri (and ideally verification_uri_complete) as strings in the device authorization response, per RFC 8628 section 3.2.","Check any reverse proxy/API gateway response-rewriting rules and whitelist verification_uri / verification_uri_complete so they are not stripped.","If the server cannot be changed, perform the device flow manually (poll the token endpoint with the returned device_code) instead of using the browser-based login path."],"exampleFix":"// before (server response)\n{\"device_code\":\"<opaque>\",\"user_code\":\"ABCD-EFGH\",\"expires_in\":600}\n// after\n{\"device_code\":\"<opaque>\",\"user_code\":\"ABCD-EFGH\",\"expires_in\":600,\"verification_uri\":\"https://caveman.internal.example/device\",\"verification_uri_complete\":\"https://caveman.internal.example/device?code=ABCD-EFGH\"}","handlingStrategy":"validation","validationCode":"function hasVerificationUri(c) {\n  const v = c.verification_uri_complete ?? c.verification_uri;\n  return typeof v === \"string\" && v.length > 0;\n}","typeGuard":"function hasStringVerificationUri(c) {\n  const v = c.verification_uri_complete ?? c.verification_uri;\n  return typeof v === \"string\" && v.length > 0;\n}","tryCatchPattern":"try {\n  await caveman.login({ instance });\n} catch (e) {\n  if (e instanceof Error && e.message === \"private device authorization omitted its browser URL\") {\n    console.error(\"Enable verification_uri/verification_uri_complete on the authorization server, or complete the device flow via token polling.\");\n  } else throw e;\n}","preventionTips":["Enable the verification_uri response field in your authorization server's device-flow configuration.","Verify proxies do not strip unknown response fields from the device authorization payload.","Add an integration test asserting verification_uri is present in the device authorization response."],"tags":["oauth","device-flow","missing-field","verification-uri","login"],"backgroundTag":"unexpected-response-shape","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}