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

  1. 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.
  2. Check any reverse proxy/API gateway response-rewriting rules and whitelist verification_uri / verification_uri_complete so they are not stripped.
  3. 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

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


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)