JuliusBrussee/caveman · error
private device authorization omitted its browser URL
Error message
private device authorization omitted its browser URL
What it means
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.
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.
Example fix
// before (server response)
{"device_code":"<opaque>","user_code":"ABCD-EFGH","expires_in":600}
// after
{"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"} Defensive patterns
Strategy: validation
Validate before calling
function hasVerificationUri(c) {
const v = c.verification_uri_complete ?? c.verification_uri;
return typeof v === "string" && v.length > 0;
} Type guard
function hasStringVerificationUri(c) {
const v = c.verification_uri_complete ?? c.verification_uri;
return typeof v === "string" && v.length > 0;
} Try / catch
try {
await caveman.login({ instance });
} catch (e) {
if (e instanceof Error && e.message === "private device authorization omitted its browser URL") {
console.error("Enable verification_uri/verification_uri_complete on the authorization server, or complete the device flow via token polling.");
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- private device authorization returned an invalid code…
- device authorization failed: HTTP
- device authorization failed: missing device code
- device credential delivery acknowledgement failed
- device login failed
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/667aef85a0ec3610.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:9605
throw new Error("--instance requires a private HTTPS origin (HTTP loopback is allowed for local development)");
}
return { noBrowser, instance: url.origin };
}
function secureLoginURL(url: URL, allowLoopback = true): boolean {
return !url.username && !url.password && (url.protocol === "https:" ||
(allowLoopback && url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)));
}
function privateVerificationURL(code: Record<string, unknown>, instance: string): string {
if (typeof code.device_code !== "string" || !code.device_code || code.device_code.length > 4096 ||
typeof code.user_code !== "string" || !/^[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/.test(code.user_code) ||
typeof code.expires_in !== "number" || !Number.isFinite(code.expires_in) || code.expires_in <= 0 || code.expires_in > 3600 ||
(code.interval !== undefined && (typeof code.interval !== "number" || !Number.isFinite(code.interval) || code.interval < 0 || code.interval > 60))) {
throw new Error("private device authorization returned an invalid code response");
}
const value = code.verification_uri_complete ?? code.verification_uri;
if (typeof value !== "string") throw new Error("private device authorization omitted its browser URL");
const url = new URL(value);
if (!secureLoginURL(url, new URL(instance).protocol === "http:") || url.hash) {
throw new Error("private device authorization returned an unsafe browser URL");
}
url.searchParams.set("user_code", code.user_code);
url.searchParams.set("connection", "mcp");
url.searchParams.set("client_name", "Caveman CLI");
return url.href;
}
function openLoginBrowser(url: string): void {
const opener = loginBrowserOpener(url);
if (!which(opener.command)) {
process.stderr.write(` browser opener unavailable; open ${url}\n`);
return;
}
const child = spawn(opener.command, opener.args, { detached: true, stdio: "ignore", windowsHide: true });
child.once("error", () => process.stderr.write(` browser did not open; open ${url}\n`));View on GitHub (pinned to 3ee70a1026)