mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(core): reject GitHub Copilot login without chat entitlement (#46959)
This commit is contained in:
@@ -30,6 +30,8 @@ const Token = Schema.Struct({
|
||||
interval: Schema.optional(Schema.Number),
|
||||
})
|
||||
const User = Schema.Struct({
|
||||
chat_enabled: Schema.optional(Schema.Boolean),
|
||||
can_signup_for_limited: Schema.optional(Schema.Boolean),
|
||||
endpoints: Schema.optional(
|
||||
Schema.Struct({
|
||||
api: Schema.optional(Schema.String),
|
||||
@@ -107,23 +109,30 @@ const oauth = (app: App.Info) =>
|
||||
},
|
||||
},
|
||||
).pipe(
|
||||
Effect.map((user) => Option.getOrUndefined(decodeUser(user))?.endpoints?.api?.replace(/\/+$/, "")),
|
||||
Effect.map((user) => Option.getOrUndefined(decodeUser(user))),
|
||||
// Only an explicit entitlement answer blocks login; a failed
|
||||
// or malformed lookup must not turn a GitHub hiccup into a denial.
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
Effect.map((apiEndpoint) =>
|
||||
Credential.OAuth.make({
|
||||
type: "oauth",
|
||||
methodID,
|
||||
refresh: access,
|
||||
access,
|
||||
expires: 0,
|
||||
...((enterprise || apiEndpoint) && {
|
||||
metadata: {
|
||||
...(enterprise ? { enterpriseUrl: domain } : {}),
|
||||
...(apiEndpoint ? { apiEndpoint } : {}),
|
||||
},
|
||||
Effect.flatMap((user) => {
|
||||
const denied = user && copilotEntitlementError(user)
|
||||
if (denied) return Effect.fail(new Error(denied))
|
||||
const apiEndpoint = user?.endpoints?.api?.replace(/\/+$/, "")
|
||||
return Effect.succeed(
|
||||
Credential.OAuth.make({
|
||||
type: "oauth",
|
||||
methodID,
|
||||
refresh: access,
|
||||
access,
|
||||
expires: 0,
|
||||
...((enterprise || apiEndpoint) && {
|
||||
metadata: {
|
||||
...(enterprise ? { enterpriseUrl: domain } : {}),
|
||||
...(apiEndpoint ? { apiEndpoint } : {}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (token.error === "authorization_pending")
|
||||
@@ -298,6 +307,15 @@ function baseURL(enterprise?: string) {
|
||||
return enterprise ? `https://copilot-api.${normalizeDomain(enterprise)}` : "https://api.githubcopilot.com"
|
||||
}
|
||||
|
||||
// GitHub reports Copilot access on /copilot_internal/user; OAuth itself succeeds
|
||||
// for any GitHub account, so this is the only signal that the account can chat.
|
||||
export function copilotEntitlementError(user: { chat_enabled?: boolean; can_signup_for_limited?: boolean }) {
|
||||
if (user.chat_enabled !== false) return
|
||||
if (user.can_signup_for_limited)
|
||||
return "This GitHub account is not signed up for GitHub Copilot. Sign up for Copilot Free at https://github.com/features/copilot/plans and connect again."
|
||||
return "This GitHub account does not have GitHub Copilot access. It needs an active Copilot subscription or a seat assigned by an organization."
|
||||
}
|
||||
|
||||
export function copilotBaseURL(metadata?: Readonly<Record<string, unknown>>) {
|
||||
const endpoint = metadata?.apiEndpoint
|
||||
if (typeof endpoint === "string" && endpoint) return endpoint
|
||||
|
||||
@@ -10,7 +10,12 @@ import { ModelResolver } from "@opencode-ai/core/model-resolver"
|
||||
import { Plugin } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||
import { copilotBaseURL, copilotFetch, GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
|
||||
import {
|
||||
copilotBaseURL,
|
||||
copilotEntitlementError,
|
||||
copilotFetch,
|
||||
GithubCopilotPlugin,
|
||||
} from "@opencode-ai/core/plugin/provider/github-copilot"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { fakeSelectorSdk } from "../fixture/selector"
|
||||
@@ -40,6 +45,20 @@ describe("GithubCopilotPlugin", () => {
|
||||
).toBe("https://api.business.githubcopilot.com")
|
||||
})
|
||||
|
||||
test("rejects accounts without Copilot chat access", () => {
|
||||
expect(copilotEntitlementError({ chat_enabled: false, can_signup_for_limited: true })).toContain("Copilot Free")
|
||||
expect(copilotEntitlementError({ chat_enabled: false, can_signup_for_limited: false })).toContain(
|
||||
"subscription or a seat",
|
||||
)
|
||||
expect(copilotEntitlementError({ chat_enabled: false })).toContain("subscription or a seat")
|
||||
})
|
||||
|
||||
test("only blocks on an explicit entitlement denial", () => {
|
||||
expect(copilotEntitlementError({ chat_enabled: true })).toBeUndefined()
|
||||
expect(copilotEntitlementError({ chat_enabled: true, can_signup_for_limited: true })).toBeUndefined()
|
||||
expect(copilotEntitlementError({})).toBeUndefined()
|
||||
})
|
||||
|
||||
it.effect("registers GitHub Copilot device OAuth", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin()
|
||||
|
||||
Reference in New Issue
Block a user