Files
openclaw/src/plugin-sdk/github-copilot-token-endpoint.ts
Yuval Dinodia d576881390 fix(github-copilot): reject unsupported OAuth enterprise domain before refresh and model routing (#105584)
* fix(github-copilot): reject unsupported OAuth enterprise domain before refresh and model routing

Legacy github-copilot OAuth credentials can carry a non-github.com
enterpriseUrl. The token-refresh path templated it into the endpoint and
sent the bearer refresh token there with no allowlist, and the model
routing path derived a base URL from the same credential, so an unexpired
credential kept routing its access token to that origin without ever
reaching a refresh.

A persisted credential origin that is not on the Copilot host allowlist
is now rejected before any request is issued and before any model URL is
produced. Because an off-allowlist origin means the stored access token
may itself have been minted by that origin, the credential is refused as
a whole rather than coerced to github.com: modifyModels withholds the
github-copilot models so the token has no route, and its proxy-ep is
never trusted. Login rejects an unsupported host up front, so every path
that turns a domain into a bearer-bearing URL validates first.
formatAuthDoctorHint guides affected users to re-authenticate, mirroring
the qwen-portal hint. Supported hosts are unchanged: github.com and
*.ghe.com data-residency tenants still route and refresh as before.

The allowlist plus a new isSupportedGithubCopilotDomain predicate move to
a dependency-free plugin-sdk leaf so the core OAuth runtime can share them
without closing a module cycle; plugin-sdk/provider-auth re-exports
normalizeGithubCopilotDomain, so the plugin-owned GitHub Copilot provider
keeps its existing import.

Fixes #103078.

* fix(github-copilot): resolve provider auth base conflict

* fix(github-copilot): resolve provider auth base conflict

* fix(github-copilot): validate token proxy endpoint

* fix(github-copilot): remove stale helper import

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
2026-07-14 22:29:35 -07:00

59 lines
1.9 KiB
TypeScript

import {
DEFAULT_GITHUB_COPILOT_DOMAIN,
isSupportedGithubCopilotDomain,
normalizeGithubCopilotDomain,
} from "./github-copilot-domain.js";
export type GithubCopilotTokenEndpointResolution = {
hasProxyEndpoint: boolean;
baseUrl: string | null;
};
function isSupportedGithubCopilotApiHost(host: string, enterpriseDomain?: string): boolean {
if (host === "copilot-proxy.githubusercontent.com" || host.endsWith(".githubcopilot.com")) {
return true;
}
if (
!enterpriseDomain ||
!isSupportedGithubCopilotDomain(enterpriseDomain) ||
normalizeGithubCopilotDomain(enterpriseDomain) === DEFAULT_GITHUB_COPILOT_DOMAIN
) {
return false;
}
const tenant = normalizeGithubCopilotDomain(enterpriseDomain);
return host === tenant || host.endsWith(`.${tenant}`);
}
/**
* Resolves the optional `proxy-ep` hint embedded in a Copilot API token.
* The hint is untrusted credential data: only GitHub-owned Copilot hosts, or
* service hosts below the credential's validated GHE.com tenant, may receive it.
*/
export function resolveGithubCopilotTokenEndpoint(
token: string,
enterpriseDomain?: string,
): GithubCopilotTokenEndpointResolution {
const match = token.trim().match(/(?:^|;)\s*proxy-ep=([^;\s]+)/i);
const proxyEndpoint = match?.[1]?.trim();
if (!proxyEndpoint) {
return { hasProxyEndpoint: false, baseUrl: null };
}
const urlText = /^https?:\/\//i.test(proxyEndpoint) ? proxyEndpoint : `https://${proxyEndpoint}`;
try {
const url = new URL(urlText);
if (url.protocol !== "http:" && url.protocol !== "https:") {
return { hasProxyEndpoint: true, baseUrl: null };
}
const apiHost = url.hostname.toLowerCase().replace(/^proxy\./, "api.");
return {
hasProxyEndpoint: true,
baseUrl: isSupportedGithubCopilotApiHost(apiHost, enterpriseDomain)
? `https://${apiHost}`
: null,
};
} catch {
return { hasProxyEndpoint: true, baseUrl: null };
}
}