// Xai tests cover xai oauth plugin behavior. import type { ProviderAuthContext } from "openclaw/plugin-sdk/plugin-entry"; import { createRuntimeEnv, createTestWizardPrompter, } from "openclaw/plugin-sdk/plugin-test-runtime"; import type { OAuthCredential } from "openclaw/plugin-sdk/provider-auth"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createXaiDeviceCodeAuthMethod, createXaiOAuthAuthMethod, fetchXaiOAuthDiscovery, isTrustedXaiOAuthEndpoint, loginXaiDeviceCode, refreshXaiOAuthCredential, XAI_OAUTH_CLIENT_ID, XAI_OAUTH_DISCOVERY_URL, XAI_OAUTH_SCOPE, } from "./xai-oauth.js"; function jsonResponse(value: unknown, init?: ResponseInit): Response { return new Response(JSON.stringify(value), { status: 200, headers: { "Content-Type": "application/json" }, ...init, }); } function createJwt(payload: Record): string { const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url"); const body = Buffer.from(JSON.stringify(payload)).toString("base64url"); return `${header}.${body}.signature`; } function requireStringBody(init: RequestInit | undefined): string { if (typeof init?.body !== "string") { throw new Error("expected request body to be a string"); } return init.body; } function requestUrl(input: RequestInfo | URL): string { if (typeof input === "string") { return input; } if (input instanceof URL) { return input.href; } return input.url; } describe("xAI OAuth", () => { afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); vi.useRealTimers(); }); it("accepts only trusted xAI OAuth endpoints", () => { expect(isTrustedXaiOAuthEndpoint("https://auth.x.ai/oauth2/token")).toBe(true); expect(isTrustedXaiOAuthEndpoint("https://accounts.x.ai/oauth2/token")).toBe(true); expect(isTrustedXaiOAuthEndpoint("http://auth.x.ai/oauth2/token")).toBe(false); expect(isTrustedXaiOAuthEndpoint("https://x.ai.evil.test/oauth2/token")).toBe(false); expect(isTrustedXaiOAuthEndpoint("not a url")).toBe(false); }); it("keeps the public auth method named OAuth while using device code", () => { const method = createXaiOAuthAuthMethod(); expect(method.id).toBe("oauth"); expect(method.kind).toBe("oauth"); expect(method.wizard?.choiceId).toBe("xai-oauth"); expect(method.wizard?.methodId).toBe("oauth"); }); it("preserves device-code as an explicit auth method alias", () => { const method = createXaiDeviceCodeAuthMethod(); expect(method.id).toBe("device-code"); expect(method.kind).toBe("device_code"); expect(method.wizard?.choiceId).toBe("xai-device-code"); expect(method.wizard?.methodId).toBe("device-code"); expect(method.wizard?.assistantVisibility).toBe("manual-only"); }); it("validates discovered endpoints before using them", async () => { vi.stubEnv("OPENCLAW_VERSION", "2026.3.22"); const fetchImpl = vi.fn(async () => jsonResponse({ authorization_endpoint: "https://auth.x.ai/oauth2/authorize", token_endpoint: "https://auth.x.ai/oauth2/token", }), ); await expect(fetchXaiOAuthDiscovery({ fetchImpl })).resolves.toEqual({ tokenEndpoint: "https://auth.x.ai/oauth2/token", }); const discoveryInit = fetchImpl.mock.calls.at(0)?.[1]; const discoveryHeaders = new Headers(discoveryInit?.headers ?? {}); expect(discoveryHeaders.get("user-agent")).toBe("openclaw/2026.3.22"); vi.unstubAllEnvs(); const poisonedFetch = vi.fn(async () => jsonResponse({ authorization_endpoint: "https://auth.x.ai/oauth2/authorize", token_endpoint: "https://evil.test/oauth2/token", }), ); await expect(fetchXaiOAuthDiscovery({ fetchImpl: poisonedFetch })).rejects.toThrow( "untrusted token endpoint", ); }); it("refreshes with the cached token endpoint and preserves refresh fallback", async () => { vi.stubEnv("OPENCLAW_VERSION", "2026.3.22"); const fetchImpl = vi.fn(async (_url, init) => { expect(init?.method).toBe("POST"); expect(typeof init?.body).toBe("string"); const body = requireStringBody(init); expect(body).toContain("grant_type=refresh_token"); expect(body).toContain(`client_id=${encodeURIComponent(XAI_OAUTH_CLIENT_ID)}`); expect(body).toContain("refresh_token=refresh-1"); const headers = new Headers(init?.headers ?? {}); expect(headers.get("user-agent")).toBe("openclaw/2026.3.22"); return jsonResponse({ access_token: "access-2", expires_in: 120, }); }); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth2/token", } satisfies OAuthCredential & { tokenEndpoint: string }; const refreshed = await refreshXaiOAuthCredential(credential, { fetchImpl, now: () => 1_000 }); expect(fetchImpl).toHaveBeenCalledWith("https://auth.x.ai/oauth2/token", expect.any(Object)); expect(refreshed.access).toBe("access-2"); expect(refreshed.refresh).toBe("refresh-1"); expect(refreshed.expires).toBe(121_000); }); it("rediscovers the current token endpoint for stale xAI OAuth credentials", async () => { const fetchImpl = vi.fn(async (url, init) => { if (requestUrl(url) === XAI_OAUTH_DISCOVERY_URL) { expect(init?.method).toBeUndefined(); return jsonResponse({ authorization_endpoint: "https://auth.x.ai/oauth2/authorize", token_endpoint: "https://auth.x.ai/oauth2/token", }); } expect(requestUrl(url)).toBe("https://auth.x.ai/oauth2/token"); expect(init?.method).toBe("POST"); expect(requireStringBody(init)).toContain("refresh_token=refresh-1"); return jsonResponse({ access_token: "access-2", refresh_token: "refresh-2", expires_in: 120, }); }); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth/token", } satisfies OAuthCredential & { tokenEndpoint: string }; const refreshed = await refreshXaiOAuthCredential(credential, { fetchImpl, now: () => 1_000 }); expect(fetchImpl).toHaveBeenCalledTimes(2); expect(fetchImpl.mock.calls.map(([url]) => requestUrl(url))).toEqual([ XAI_OAUTH_DISCOVERY_URL, "https://auth.x.ai/oauth2/token", ]); expect(refreshed).toMatchObject({ access: "access-2", refresh: "refresh-2", tokenEndpoint: "https://auth.x.ai/oauth2/token", }); }); it("does not reuse the stale xAI OAuth token endpoint when discovery fails", async () => { const fetchImpl = vi.fn(async (url) => { expect(requestUrl(url)).toBe(XAI_OAUTH_DISCOVERY_URL); throw new Error("discovery unavailable"); }); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth/token", } satisfies OAuthCredential & { tokenEndpoint: string }; await expect(refreshXaiOAuthCredential(credential, { fetchImpl })).rejects.toThrow( "discovery unavailable", ); expect(fetchImpl).toHaveBeenCalledTimes(1); }); it("retries transient HTML refresh failures before succeeding", async () => { vi.useFakeTimers(); const fetchImpl = vi .fn() .mockResolvedValueOnce( new Response("Attention Required! Cloudflare", { status: 403, headers: { "Content-Type": "text/html", "cf-mitigated": "challenge", }, }), ) .mockResolvedValueOnce( new Response("Just a moment...", { status: 403, headers: { "Content-Type": "text/html", }, }), ) .mockResolvedValueOnce( jsonResponse({ access_token: "access-2", expires_in: 120, }), ); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth2/token", } satisfies OAuthCredential & { tokenEndpoint: string }; const refresh = refreshXaiOAuthCredential(credential, { fetchImpl, now: () => 1_000 }); await vi.advanceTimersByTimeAsync(250); await vi.advanceTimersByTimeAsync(250); const refreshed = await refresh; expect(fetchImpl).toHaveBeenCalledTimes(3); expect(refreshed.access).toBe("access-2"); expect(refreshed.refresh).toBe("refresh-1"); }); it("surfaces xAI Cloudflare refresh failures after retry exhaustion", async () => { vi.useFakeTimers(); const fetchImpl = vi.fn( async () => new Response( "Attention Required! | CloudflareYou are unable to access x.ai", { status: 403, headers: { "Content-Type": "text/html", "cf-mitigated": "challenge", }, }, ), ); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth2/token", } satisfies OAuthCredential & { tokenEndpoint: string }; const refresh = refreshXaiOAuthCredential(credential, { fetchImpl, now: () => 1_000 }); const expectation = expect(refresh).rejects.toThrow( "xAI returned an HTML/Cloudflare challenge", ); await vi.advanceTimersByTimeAsync(250); await vi.advanceTimersByTimeAsync(250); await expectation; expect(fetchImpl).toHaveBeenCalledTimes(3); }); it("does not retry terminal xAI OAuth refresh errors", async () => { const fetchImpl = vi.fn(async () => jsonResponse( { error: "invalid_grant", error_description: "Invalid or unknown refresh token", }, { status: 400 }, ), ); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth2/token", } satisfies OAuthCredential & { tokenEndpoint: string }; await expect(refreshXaiOAuthCredential(credential, { fetchImpl })).rejects.toThrow( "invalid_grant (Invalid or unknown refresh token)", ); expect(fetchImpl).toHaveBeenCalledTimes(1); }); it("does not retry refresh-token service failures", async () => { const fetchImpl = vi.fn(async () => jsonResponse( { error: "server_error", error_description: "try again later", }, { status: 503 }, ), ); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth2/token", } satisfies OAuthCredential & { tokenEndpoint: string }; await expect(refreshXaiOAuthCredential(credential, { fetchImpl })).rejects.toThrow( "server_error (try again later)", ); expect(fetchImpl).toHaveBeenCalledTimes(1); }); it("does not retry refresh on transport errors so a rotated refresh token is never resent", async () => { const fetchImpl = vi.fn(async () => { throw new Error("socket hang up"); }); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth2/token", } satisfies OAuthCredential & { tokenEndpoint: string }; await expect(refreshXaiOAuthCredential(credential, { fetchImpl })).rejects.toThrow( "socket hang up", ); expect(fetchImpl).toHaveBeenCalledTimes(1); }); it("does not coerce partial xAI expires_in values", async () => { const fetchImpl = vi.fn(async () => jsonResponse({ access_token: "access-2", expires_in: "120s", }), ); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth2/token", } satisfies OAuthCredential & { tokenEndpoint: string }; const refreshed = await refreshXaiOAuthCredential(credential, { fetchImpl, now: () => 1_000 }); expect(refreshed.expires).toBe(100); }); it("preserves the cached xAI expiry when token lifetimes overflow safe milliseconds", async () => { const fetchImpl = vi.fn(async () => jsonResponse({ access_token: createJwt({ exp: Number.MAX_SAFE_INTEGER }), expires_in: Number.MAX_SAFE_INTEGER, }), ); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth2/token", } satisfies OAuthCredential & { tokenEndpoint: string }; const refreshed = await refreshXaiOAuthCredential(credential, { fetchImpl, now: () => 1_000 }); expect(refreshed.expires).toBe(100); }); it("ignores unsafe JWT expiry fallbacks from xAI access tokens", async () => { const fetchImpl = vi.fn(async () => jsonResponse({ access_token: createJwt({ exp: Number.MAX_SAFE_INTEGER }), }), ); const credential = { type: "oauth", provider: "xai", access: "access-1", refresh: "refresh-1", expires: 100, tokenEndpoint: "https://auth.x.ai/oauth2/token", } satisfies OAuthCredential & { tokenEndpoint: string }; const refreshed = await refreshXaiOAuthCredential(credential, { fetchImpl, now: () => 1_000 }); expect(refreshed.expires).toBe(100); }); it("logs in with xAI device code without a localhost callback", async () => { vi.stubEnv("OPENCLAW_VERSION", "2026.3.22"); const progress = { update: vi.fn(), stop: vi.fn(), }; const fetchImpl = vi .fn() .mockResolvedValueOnce( jsonResponse({ authorization_endpoint: "https://auth.x.ai/oauth2/authorize", device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code", token_endpoint: "https://auth.x.ai/oauth2/token", }), ) .mockResolvedValueOnce( jsonResponse({ device_code: "device-code-1", user_code: "ABCD-1234", verification_uri: "https://accounts.x.ai/oauth2/device", verification_uri_complete: "https://accounts.x.ai/oauth2/device?user_code=ABCD-1234", expires_in: 900, interval: 5, }), ) .mockResolvedValueOnce( jsonResponse({ access_token: createJwt({ exp: 4, sub: "acct-1" }), refresh_token: "refresh-1", id_token: createJwt({ sub: "acct-1", email: "dev@example.com", name: "Dev User", }), expires_in: 120, }), ); vi.stubGlobal("fetch", fetchImpl); const note = vi.fn<(message: string, title?: string) => Promise>(async () => {}); const openUrl = vi.fn(async () => {}); const log = vi.fn(); const runtime = { ...createRuntimeEnv(), log }; const ctx: ProviderAuthContext = { config: {}, isRemote: true, openUrl, prompter: createTestWizardPrompter({ progress: vi.fn(() => progress), note, }), runtime, oauth: { createVpsAwareHandlers: () => { throw new Error("unexpected VPS OAuth handler request"); }, }, }; const result = await loginXaiDeviceCode(ctx); expect(openUrl).not.toHaveBeenCalled(); expect(note).toHaveBeenCalledWith(expect.stringContaining("ABCD-1234"), "xAI OAuth"); const remoteLog = log.mock.calls[0]?.[0]; expect(remoteLog).toContain("https://accounts.x.ai/oauth2/device"); expect(remoteLog).not.toContain("ABCD-1234"); const deviceRequest = fetchImpl.mock.calls[1]?.[1]; expect(deviceRequest?.method).toBe("POST"); const deviceBody = requireStringBody(deviceRequest); expect(deviceBody).toContain(`client_id=${encodeURIComponent(XAI_OAUTH_CLIENT_ID)}`); expect(deviceBody).toContain(`scope=${encodeURIComponent(XAI_OAUTH_SCOPE)}`); const tokenRequest = fetchImpl.mock.calls[2]?.[1]; expect(tokenRequest?.method).toBe("POST"); const tokenBody = requireStringBody(tokenRequest); expect(tokenBody).toContain( "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code", ); expect(tokenBody).toContain("device_code=device-code-1"); expect(result.profiles[0]?.credential).toMatchObject({ type: "oauth", provider: "xai", refresh: "refresh-1", email: "dev@example.com", displayName: "Dev User", tokenEndpoint: "https://auth.x.ai/oauth2/token", deviceAuthorizationEndpoint: "https://auth.x.ai/oauth2/device/code", issuer: "https://auth.x.ai", authFlow: "device-code", accountId: "acct-1", access: expect.any(String), }); expect(progress.update).toHaveBeenCalledWith("Waiting for xAI device authorization..."); expect(progress.stop).toHaveBeenCalledWith("xAI OAuth complete"); }); it("falls back for unsafe xAI device-code lifetime fields", async () => { const progress = { update: vi.fn(), stop: vi.fn(), }; const fetchImpl = vi .fn() .mockResolvedValueOnce( jsonResponse({ authorization_endpoint: "https://auth.x.ai/oauth2/authorize", device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code", token_endpoint: "https://auth.x.ai/oauth2/token", }), ) .mockResolvedValueOnce( jsonResponse({ device_code: "device-code-1", user_code: "ABCD-1234", verification_uri: "https://accounts.x.ai/oauth2/device", expires_in: Number.MAX_SAFE_INTEGER, interval: Number.MAX_SAFE_INTEGER, }), ) .mockResolvedValueOnce( jsonResponse({ access_token: "access-token", refresh_token: "refresh-1", expires_in: 120, }), ); vi.stubGlobal("fetch", fetchImpl); const note = vi.fn<(message: string, title?: string) => Promise>(async () => {}); const ctx: ProviderAuthContext = { config: {}, isRemote: true, openUrl: vi.fn(async () => {}), prompter: createTestWizardPrompter({ progress: vi.fn(() => progress), note, }), runtime: createRuntimeEnv(), oauth: { createVpsAwareHandlers: () => { throw new Error("unexpected VPS OAuth handler request"); }, }, }; await loginXaiDeviceCode(ctx); expect(note).toHaveBeenCalledWith( expect.stringContaining("Code expires in 5 minutes."), "xAI OAuth", ); expect(progress.stop).toHaveBeenCalledWith("xAI OAuth complete"); }); });