



























@@ -7,6 +7,7 @@ import {
77upsertAuthProfileWithLock,
88} from "openclaw/plugin-sdk/provider-auth";
99import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
10+import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
10111112const CLIENT_ID = "Iv1.b507a08c87ecfe98";
1213const DEVICE_CODE_URL = "https://github.com/login/device/code";
@@ -47,6 +48,14 @@ class GitHubDeviceFlowError extends Error {
4748}
4849}
495051+let githubDeviceFlowFetchGuard = fetchWithSsrFGuard;
52+53+export function _setGitHubCopilotDeviceFlowFetchGuardForTesting(
54+impl: typeof fetchWithSsrFGuard | null,
55+): void {
56+githubDeviceFlowFetchGuard = impl ?? fetchWithSsrFGuard;
57+}
58+5059async function upsertAuthProfileWithLockOrThrow(params: UpsertAuthProfileParams): Promise<void> {
5160const updated = await upsertAuthProfileWithLock(params);
5261if (!updated) {
@@ -71,26 +80,45 @@ function parseJsonResponse(value: unknown): Record<string, unknown> {
7180return value as Record<string, unknown>;
7281}
738283+async function postGitHubDeviceFlowForm(params: {
84+url: string;
85+body: URLSearchParams;
86+failureLabel: string;
87+}): Promise<Record<string, unknown>> {
88+const { response, release } = await githubDeviceFlowFetchGuard({
89+url: params.url,
90+init: {
91+method: "POST",
92+headers: {
93+Accept: "application/json",
94+"Content-Type": "application/x-www-form-urlencoded",
95+},
96+body: params.body,
97+},
98+requireHttps: true,
99+auditContext: "github-copilot-device-flow",
100+});
101+try {
102+if (!response.ok) {
103+throw new Error(`${params.failureLabel}: HTTP ${response.status}`);
104+}
105+return parseJsonResponse(await response.json());
106+} finally {
107+await release();
108+}
109+}
110+74111async function requestDeviceCode(params: { scope: string }): Promise<DeviceCodeResponse> {
75112const body = new URLSearchParams({
76113client_id: CLIENT_ID,
77114scope: params.scope,
78115});
7911680-const res = await fetch(DEVICE_CODE_URL, {
81-method: "POST",
82-headers: {
83-Accept: "application/json",
84-"Content-Type": "application/x-www-form-urlencoded",
85-},
117+const json = (await postGitHubDeviceFlowForm({
118+url: DEVICE_CODE_URL,
86119 body,
87-});
88-89-if (!res.ok) {
90-throw new Error(`GitHub device code failed: HTTP ${res.status}`);
91-}
92-93-const json = parseJsonResponse(await res.json()) as DeviceCodeResponse;
120+failureLabel: "GitHub device code failed",
121+})) as DeviceCodeResponse;
94122if (!json.device_code || !json.user_code || !json.verification_uri) {
95123throw new Error("GitHub device code response missing fields");
96124}
@@ -109,20 +137,11 @@ async function pollForAccessToken(params: {
109137});
110138111139while (Date.now() < params.expiresAt) {
112-const res = await fetch(ACCESS_TOKEN_URL, {
113-method: "POST",
114-headers: {
115-Accept: "application/json",
116-"Content-Type": "application/x-www-form-urlencoded",
117-},
140+const json = (await postGitHubDeviceFlowForm({
141+url: ACCESS_TOKEN_URL,
118142body: bodyBase,
119-});
120-121-if (!res.ok) {
122-throw new Error(`GitHub device token failed: HTTP ${res.status}`);
123-}
124-125-const json = parseJsonResponse(await res.json()) as DeviceTokenResponse;
143+failureLabel: "GitHub device token failed",
144+})) as DeviceTokenResponse;
126145if ("access_token" in json && typeof json.access_token === "string") {
127146return json.access_token;
128147}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。