Files
OmniRoute/src/shared/utils/clineAuth.ts
Imam Wahyu Widodo d3331f8bca feat(providers): ClinePass OAuth login — dual-auth on top of #5942 (reconciles #5924) (#6126)
* feat(providers): rebase ClinePass dual-auth (OAuth + BYOK) onto release/v3.8.47

ClinePass now offers both sign-in methods on its dashboard page: OAuth
(reusing the Cline WorkOS flow, primary "Connect" button) or a pasted
BYOK API key ("Manual API key"), instead of only the API-key-only
provider shipped in #5942.

- Registry: authType oauth + oauth urls, alias aligned to "cp" (matches
  the OAUTH_PROVIDERS catalog alias so <alias>/<modelId> routing
  resolves); keeps the #6165 forceStream:true fix (streaming-only API).
- Executor: new buildClinepassHeaders() (src/shared/utils/clineAuth.ts)
  picks buildClineHeaders() for an OAuth accessToken or a plain Bearer +
  Cline identification headers for a BYOK key — extracted to a leaf
  module to avoid growing the frozen open-sse/executors/default.ts.
- Refresh: dispatch clinepass to the shared refreshClineToken() (was
  falling through to the generic refresh and failing silently).
- Catalog: admit the BYOK path through a dedicated
  DUAL_AUTH_APIKEY_PROVIDER_IDS gate (src/lib/providers/catalog.ts) so
  POST /api/providers accepts an apikey connection without flipping
  isOAuth off (which would break the primary Connect->OAuth routing).
- Dashboard: render both "Connect" + "Manual API key" buttons for
  clinepass (ConnectionsHeaderToolbar.tsx, EmptyConnectionsPlaceholder.tsx).
- Dedup: removed the now-redundant API-key-only APIKEY_PROVIDERS_GATEWAYS
  entry so ClinePass is listed once (OAuth-primary).
- oauth.ts: added the clinepass catalog entry (was reverted by staleness
  during rebase); src/lib/oauth/providers/index.ts: clinepass -> cline.

This branch was ~167 commits / weeks behind release/v3.8.47; a real
merge surfaced 61 conflicting files, several of which are already-shipped
fixes (forceStream #6165, zed-hosted, requesty, agentrouter CC-wire-image,
NVIDIA/Mistral/kimi executor fixes, chatCore hardening) that a naive
resolution would have silently reverted. Reconstructed clean on top of
current release/v3.8.47, isolating and re-applying only the clinepass
dual-auth feature and preserving every already-shipped fix untouched.

tokenRefresh.ts's frozen-file cap raised by the irreducible 1-line
`case "clinepass":` switch label (config/quality/file-size-baseline.json,
justified inline); open-sse/executors/default.ts stays under its cap via
the buildClinepassHeaders() extraction.

Regression guard: tests/unit/clinepass-provider.test.ts (15/15, extended
with the dual-auth admission-gate and alias-consistency guards).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(providers): update APIKEY_PROVIDERS spread-merge count 171->170

The ClinePass dual-auth rebase (this PR) removed the now-redundant
API-key-only APIKEY_PROVIDERS_GATEWAYS.clinepass entry (dedup — clinepass
is OAuth-primary now, with its BYOK path admitted through the
DUAL_AUTH_APIKEY_PROVIDER_IDS gate instead of a second catalog entry),
which drops the total APIKEY_PROVIDERS spread-merge count by one.
tests/unit/providers-constants-split.test.ts hardcoded the prior count
(171); updated to 170 to match, confirmed via CI (Unit Tests fast-path
1/2 and 2/2 both failed on the stale count).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(oauth): register clinepass in PROVIDERS enum to fix Unknown provider error

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(test): rebaseline oauth-providers-config.test.ts frozen size for clinepass entries

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(changelog): re-restore #6126 bullet after release sync

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: hajilok <hajilok@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-09 20:37:40 -03:00

86 lines
3.0 KiB
TypeScript

/**
* 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;
}
/**
* Build headers for a ClinePass request. ClinePass is dual-auth: an OAuth
* connection (workos:-prefixed token in `accessToken`) needs the full Cline
* client header set from `buildClineHeaders()`; a BYOK API-key connection
* (`sk_...` key, #5942) sends the key as a plain Bearer token — no `workos:`
* prefix — alongside the Cline identification headers.
*/
export function buildClinepassHeaders(
credentials: { accessToken?: unknown; apiKey?: unknown } | null | undefined,
effectiveKey?: string
): Record<string, string> {
if (credentials?.accessToken) {
return buildClineHeaders(credentials.accessToken);
}
const headers: Record<string, string> = {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
};
const byokKey = effectiveKey || (credentials?.apiKey as string | undefined);
if (byokKey) headers.Authorization = `Bearer ${byokKey}`;
return headers;
}