fix(sse): use workos auth token shape for cline (#4787)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 23:01:32 -03:00
committed by GitHub
parent 4259b104dd
commit b9fdcfc006
4 changed files with 148 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ import { buildMaritalkChatUrl } from "../config/maritalk.ts";
import { LOCAL_PROVIDERS } from "@/shared/constants/providers";
import { isForbiddenCustomHeaderName } from "@/shared/constants/upstreamHeaders";
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
import { buildClineHeaders } from "@/shared/utils/clineAuth";
import type { PoolConfig } from "../services/sessionPool/types.ts";
@@ -447,6 +448,12 @@ export class DefaultExecutor extends BaseExecutor {
case "glm-coding-apikey":
headers["x-api-key"] = effectiveKey || credentials.accessToken;
break;
case "cline":
// Cline's API requires the bearer token prefixed with `workos:` plus a
// set of Cline client-identification headers; plain `Bearer <token>`
// is rejected upstream. buildClineHeaders() emits both.
Object.assign(headers, buildClineHeaders(effectiveKey || credentials.accessToken));
break;
default:
if (isClaudeCodeCompatible(this.provider)) {
const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults(

View File

@@ -7,6 +7,7 @@ import {
joinClaudeCodeCompatibleUrl,
} from "./claudeCodeCompatible.ts";
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
import { buildClineHeaders } from "@/shared/utils/clineAuth";
const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
const OPENAI_COMPATIBLE_DEFAULTS = {
@@ -347,6 +348,11 @@ export function buildProviderHeaders(provider, credentials, stream = true, body
if (!stream) {
headers["Accept"] = "application/json";
}
} else if (provider === "cline") {
// Cline's API requires the bearer token prefixed with `workos:` plus a set
// of Cline client-identification headers; plain `Bearer <token>` is rejected
// upstream. buildClineHeaders() emits both.
Object.assign(headers, buildClineHeaders(credentials.apiKey || credentials.accessToken));
} else if (entry) {
// Registry-driven auth
const authHeader = entry.authHeader || "bearer";

View File

@@ -0,0 +1,62 @@
/**
* Cline (cline.bot) auth-shape helpers.
*
* Cline's API expects the bearer token to be prefixed with `workos:` (the
* upstream auth provider), and a set of Cline client-identification headers
* (HTTP-Referer / X-Title / X-CLIENT-* / X-PLATFORM*). Plain `Bearer <token>`
* without the `workos:` prefix is rejected upstream, so every Cline request
* must route its headers through `buildClineHeaders()`.
*/
const APP_VERSION = process.env.npm_package_version || "0.0.0";
/**
* Normalize a raw Cline token into the `workos:`-prefixed access-token shape
* Cline expects. Idempotent: a token that already carries the prefix is
* returned untouched. Non-string / empty input yields an empty string.
*/
export function getClineAccessToken(token: unknown): string {
if (typeof token !== "string") return "";
const trimmed = token.trim();
if (!trimmed) return "";
return trimmed.startsWith("workos:") ? trimmed : `workos:${trimmed}`;
}
/**
* Build the full `Authorization` header value for a Cline request, or an empty
* string when no usable token is present.
*/
export function getClineAuthorizationHeader(token: unknown): string {
const accessToken = getClineAccessToken(token);
return accessToken ? `Bearer ${accessToken}` : "";
}
/**
* Build the complete Cline client header set, optionally merged with caller
* extras. The `Authorization` header is only added when a usable token is
* present (so callers can build probe headers without a token).
*/
export function buildClineHeaders(
token: unknown,
extraHeaders: Record<string, string> = {}
): Record<string, string> {
const authorization = getClineAuthorizationHeader(token);
const headers: Record<string, string> = {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
"User-Agent": `OmniRoute/${APP_VERSION}`,
"X-PLATFORM": process.platform || "unknown",
"X-PLATFORM-VERSION": process.version || "unknown",
"X-CLIENT-TYPE": "omniroute",
"X-CLIENT-VERSION": APP_VERSION,
"X-CORE-VERSION": APP_VERSION,
"X-IS-MULTIROOT": "false",
...extraHeaders,
};
if (authorization) {
headers.Authorization = authorization;
}
return headers;
}

View File

@@ -0,0 +1,73 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
buildClineHeaders,
getClineAccessToken,
getClineAuthorizationHeader,
} from "../../src/shared/utils/clineAuth.ts";
import { buildProviderHeaders } from "../../open-sse/services/provider.ts";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
test("getClineAccessToken prefixes the token with workos:", () => {
assert.equal(getClineAccessToken("abc123"), "workos:abc123");
});
test("getClineAccessToken is idempotent when already prefixed", () => {
assert.equal(getClineAccessToken("workos:abc123"), "workos:abc123");
});
test("getClineAccessToken trims and rejects empty / non-string input", () => {
assert.equal(getClineAccessToken(" abc123 "), "workos:abc123");
assert.equal(getClineAccessToken(" "), "");
assert.equal(getClineAccessToken(""), "");
assert.equal(getClineAccessToken(undefined), "");
assert.equal(getClineAccessToken(null), "");
assert.equal(getClineAccessToken(42), "");
});
test("getClineAuthorizationHeader builds a workos-prefixed bearer header", () => {
assert.equal(getClineAuthorizationHeader("abc123"), "Bearer workos:abc123");
assert.equal(getClineAuthorizationHeader(""), "");
});
test("buildClineHeaders emits the full cline client header set", () => {
const headers = buildClineHeaders("abc123");
assert.equal(headers.Authorization, "Bearer workos:abc123");
assert.equal(headers["HTTP-Referer"], "https://cline.bot");
assert.equal(headers["X-Title"], "Cline");
assert.equal(headers["X-CLIENT-TYPE"], "omniroute");
assert.equal(headers["X-IS-MULTIROOT"], "false");
// Branding must be OmniRoute, never 9Router.
assert.ok(/^OmniRoute\//.test(headers["User-Agent"]));
assert.ok(!/9router/i.test(JSON.stringify(headers)));
});
test("buildClineHeaders merges extra headers and omits Authorization with no token", () => {
const headers = buildClineHeaders("", { Accept: "application/json" });
assert.equal(headers.Accept, "application/json");
assert.ok(!("Authorization" in headers));
// Client-identification headers are still present even without a token.
assert.equal(headers["X-CLIENT-TYPE"], "omniroute");
});
test("buildProviderHeaders uses the cline workos auth token shape", () => {
const headers = buildProviderHeaders("cline", { apiKey: "tok-abc" }, true);
assert.equal(headers.Authorization, "Bearer workos:tok-abc");
assert.equal(headers["HTTP-Referer"], "https://cline.bot");
assert.equal(headers["X-CLIENT-TYPE"], "omniroute");
});
test("buildProviderHeaders honors an accessToken for cline", () => {
const headers = buildProviderHeaders("cline", { accessToken: "acc-xyz" }, false);
assert.equal(headers.Authorization, "Bearer workos:acc-xyz");
});
test("DefaultExecutor.buildHeaders uses the cline workos auth token shape", () => {
const executor = new DefaultExecutor("cline");
const headers = executor.buildHeaders({ apiKey: "tok-abc" }, true);
assert.equal(headers.Authorization, "Bearer workos:tok-abc");
assert.equal(headers["HTTP-Referer"], "https://cline.bot");
assert.equal(headers["X-CLIENT-TYPE"], "omniroute");
assert.equal(headers["X-Title"], "Cline");
});