


























@@ -1,9 +1,10 @@
1-import { describe, expect, it, vi } from "vitest";
1+import { afterEach, describe, expect, it, vi } from "vitest";
22import {
33buildXaiOAuthAuthorizationCodeTokenBody,
44buildXaiOAuthAuthorizeUrl,
55fetchXaiOAuthDiscovery,
66isTrustedXaiOAuthEndpoint,
7+loginXaiDeviceCode,
78refreshXaiOAuthCredential,
89XAI_OAUTH_CALLBACK_CORS_ORIGIN_ALLOWLIST,
910XAI_OAUTH_CALLBACK_PORT,
@@ -20,7 +21,26 @@ function jsonResponse(value: unknown, init?: ResponseInit): Response {
2021});
2122}
222324+function createJwt(payload: Record<string, unknown>): string {
25+const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
26+const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
27+return `${header}.${body}.signature`;
28+}
29+30+function requireStringBody(init: RequestInit | undefined): string {
31+if (typeof init?.body !== "string") {
32+throw new Error("expected request body to be a string");
33+}
34+return init.body;
35+}
36+2337describe("xAI OAuth", () => {
38+afterEach(() => {
39+vi.unstubAllGlobals();
40+vi.unstubAllEnvs();
41+vi.useRealTimers();
42+});
43+2444it("accepts only trusted xAI OAuth endpoints", () => {
2545expect(isTrustedXaiOAuthEndpoint("https://auth.x.ai/oauth2/token")).toBe(true);
2646expect(isTrustedXaiOAuthEndpoint("https://accounts.x.ai/oauth2/token")).toBe(true);
@@ -80,12 +100,14 @@ describe("xAI OAuth", () => {
80100const fetchImpl = vi.fn(async () =>
81101jsonResponse({
82102authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
103+device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code",
83104token_endpoint: "https://auth.x.ai/oauth2/token",
84105}),
85106) as unknown as typeof fetch;
8610787108await expect(fetchXaiOAuthDiscovery({ fetchImpl })).resolves.toEqual({
88109authorizationEndpoint: "https://auth.x.ai/oauth2/authorize",
110+deviceAuthorizationEndpoint: "https://auth.x.ai/oauth2/device/code",
89111tokenEndpoint: "https://auth.x.ai/oauth2/token",
90112});
91113@@ -99,6 +121,7 @@ describe("xAI OAuth", () => {
99121const poisonedFetch = vi.fn(async () =>
100122jsonResponse({
101123authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
124+device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code",
102125token_endpoint: "https://evil.test/oauth2/token",
103126}),
104127) as unknown as typeof fetch;
@@ -143,4 +166,98 @@ describe("xAI OAuth", () => {
143166expect(refreshed.expires).toBe(121_000);
144167vi.unstubAllEnvs();
145168});
169+170+it("logs in with xAI device code without a localhost callback", async () => {
171+vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
172+const progress = {
173+update: vi.fn(),
174+stop: vi.fn(),
175+};
176+const fetchImpl = vi
177+.fn<typeof fetch>()
178+.mockResolvedValueOnce(
179+jsonResponse({
180+authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
181+device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code",
182+token_endpoint: "https://auth.x.ai/oauth2/token",
183+}),
184+)
185+.mockResolvedValueOnce(
186+jsonResponse({
187+device_code: "device-code-1",
188+user_code: "ABCD-1234",
189+verification_uri: "https://accounts.x.ai/oauth2/device",
190+verification_uri_complete: "https://accounts.x.ai/oauth2/device?user_code=ABCD-1234",
191+expires_in: 900,
192+interval: 5,
193+}),
194+)
195+.mockResolvedValueOnce(
196+jsonResponse({
197+access_token: createJwt({ exp: 4, sub: "acct-1" }),
198+refresh_token: "refresh-1",
199+id_token: createJwt({
200+sub: "acct-1",
201+email: "dev@example.com",
202+name: "Dev User",
203+}),
204+expires_in: 120,
205+}),
206+);
207+vi.stubGlobal("fetch", fetchImpl);
208+const ctx = {
209+config: {},
210+isRemote: true,
211+openUrl: vi.fn(async () => {}),
212+prompter: {
213+progress: vi.fn(() => progress),
214+note: vi.fn(async () => {}),
215+},
216+runtime: {
217+log: vi.fn(),
218+},
219+oauth: {},
220+};
221+222+const result = await loginXaiDeviceCode(ctx as never);
223+224+expect(ctx.openUrl).not.toHaveBeenCalled();
225+expect(ctx.prompter.note).toHaveBeenCalledWith(
226+expect.stringContaining("ABCD-1234"),
227+"xAI device code",
228+);
229+const remoteLog = ctx.runtime.log.mock.calls[0]?.[0];
230+expect(remoteLog).toContain("https://accounts.x.ai/oauth2/device");
231+expect(remoteLog).not.toContain("ABCD-1234");
232+const deviceRequest = fetchImpl.mock.calls[1]?.[1];
233+expect(deviceRequest?.method).toBe("POST");
234+const deviceBody = requireStringBody(deviceRequest);
235+expect(deviceBody).toContain(`client_id=${encodeURIComponent(XAI_OAUTH_CLIENT_ID)}`);
236+expect(deviceBody).toContain(`scope=${encodeURIComponent(XAI_OAUTH_SCOPE)}`);
237+238+const tokenRequest = fetchImpl.mock.calls[2]?.[1];
239+expect(tokenRequest?.method).toBe("POST");
240+const tokenBody = requireStringBody(tokenRequest);
241+expect(tokenBody).toContain(
242+"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code",
243+);
244+expect(tokenBody).toContain("device_code=device-code-1");
245+246+const credential = result.profiles[0]?.credential as Record<string, unknown> | undefined;
247+expect(credential).toMatchObject({
248+type: "oauth",
249+provider: "xai",
250+refresh: "refresh-1",
251+email: "dev@example.com",
252+displayName: "Dev User",
253+tokenEndpoint: "https://auth.x.ai/oauth2/token",
254+deviceAuthorizationEndpoint: "https://auth.x.ai/oauth2/device/code",
255+issuer: "https://auth.x.ai",
256+authFlow: "device-code",
257+accountId: "acct-1",
258+});
259+expect(credential?.access).toEqual(expect.any(String));
260+expect(progress.update).toHaveBeenCalledWith("Waiting for xAI device authorization...");
261+expect(progress.stop).toHaveBeenCalledWith("xAI device code complete");
262+});
146263});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。