From abc14d37e20fb1ccc24bf76e553d823719b83ff0 Mon Sep 17 00:00:00 2001 From: vkonovalchuk Date: Sun, 28 Jun 2026 23:47:05 +0300 Subject: [PATCH 1/4] fix(grok-cli): accept full auth.json object in import-token endpoint The import-token Zod schema rejected the full auth.json object (sending 400 Bad Request) because it only accepted a string token. This broke the Grok Build provider import flow in the dashboard UI, which sends the entire ~/.grok/auth.json as the token field. Changes: - oauthImportTokenSchema: accept both string and object for token - grok-cli mapTokens: extract rawAuthJson and store it in providerSpecificData for diagnostics and token refresh - extractTokenAndRefresh: typed with unknown instead of any, returns rawAuthJson alongside accessToken/refreshToken --- src/lib/oauth/providers/grok-cli.ts | 44 +++++++++++++++++++-------- src/shared/validation/schemas/auth.ts | 2 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/lib/oauth/providers/grok-cli.ts b/src/lib/oauth/providers/grok-cli.ts index 06dd771ba47..c84e6a99359 100644 --- a/src/lib/oauth/providers/grok-cli.ts +++ b/src/lib/oauth/providers/grok-cli.ts @@ -57,35 +57,52 @@ function parseJwtPayload(token: string): { * - Raw JWT string (no refresh_token available) * - The entire auth.json object: { "https://auth.x.ai::...": { "key": "eyJ...", "refresh_token": "..." } } */ -function extractTokenAndRefresh(input: any): { accessToken: string; refreshToken: string | null } { - if (typeof input === "string") return { accessToken: input, refreshToken: null }; +function extractTokenAndRefresh(input: unknown): { + accessToken: string; + refreshToken: string | null; + rawAuthJson: Record | null; +} { + if (typeof input === "string") + return { accessToken: input, refreshToken: null, rawAuthJson: null }; if (input && typeof input === "object") { // auth.json format: first entry's "key" and "refresh_token" fields const keys = Object.keys(input); - if (keys.length > 0 && input[keys[0]]?.key) { - return { - accessToken: input[keys[0]].key, - refreshToken: input[keys[0]].refresh_token || null, - }; + if ( + keys.length > 0 && + (input as Record)[keys[0]] && + typeof (input as Record)[keys[0]] === "object" + ) { + const entry = (input as Record>)[keys[0]]; + if (entry.key) { + return { + accessToken: String(entry.key), + refreshToken: typeof entry.refresh_token === "string" ? entry.refresh_token : null, + rawAuthJson: input as Record, + }; + } } // Already has accessToken - if (input.accessToken) { + if ((input as Record).accessToken) { return { - accessToken: input.accessToken, - refreshToken: input.refreshToken || null, + accessToken: String((input as Record).accessToken), + refreshToken: + typeof (input as Record).refreshToken === "string" + ? ((input as Record).refreshToken as string) + : null, + rawAuthJson: input as Record, }; } } - return { accessToken: "", refreshToken: null }; + return { accessToken: "", refreshToken: null, rawAuthJson: null }; } export const grokCli = { config: GROK_CLI_CONFIG, flowType: "import_token", - mapTokens: (token: any) => { - const { accessToken, refreshToken } = extractTokenAndRefresh(token); + mapTokens: (token: unknown) => { + const { accessToken, refreshToken, rawAuthJson } = extractTokenAndRefresh(token); const { email, authInfo } = parseJwtPayload(accessToken); return { @@ -98,6 +115,7 @@ export const grokCli = { teamId: authInfo?.team_id || null, tier: authInfo?.tier || 1, principalType: authInfo?.principal_type || "User", + rawAuthJson: rawAuthJson || undefined, }, }; }, diff --git a/src/shared/validation/schemas/auth.ts b/src/shared/validation/schemas/auth.ts index b8f22eca514..7ae8cbe8bb0 100644 --- a/src/shared/validation/schemas/auth.ts +++ b/src/shared/validation/schemas/auth.ts @@ -133,7 +133,7 @@ export const oauthPollSchema = z.object({ /** Import a raw API token (e.g. WINDSURF_API_KEY) without going through the browser OAuth flow. */ export const oauthImportTokenSchema = z.object({ - token: z.string().trim().min(1, "Token is required"), + token: z.union([z.string().trim().min(1, "Token is required"), z.record(z.unknown())]), connectionId: z.string().optional(), }); From d9b2d0a4ed97e6c209a28d5d4704b8e9ad6885d2 Mon Sep 17 00:00:00 2001 From: vkonovalchuk Date: Sun, 28 Jun 2026 23:56:05 +0300 Subject: [PATCH 2/4] fix(grok-cli): unwrap route handler wrapper in extractTokenAndRefresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The route handler wraps the token as { accessToken: token } before calling mapTokens(). The previous fix didn't account for this double wrapping — String(auth.json object) produced '[object Object]' instead of the actual JWT. Now properly unwraps { accessToken: } before scanning for the nested key/refresh_token fields. --- src/lib/oauth/providers/grok-cli.ts | 53 +++++++++++++++++------------ 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/lib/oauth/providers/grok-cli.ts b/src/lib/oauth/providers/grok-cli.ts index c84e6a99359..39ebb0448ee 100644 --- a/src/lib/oauth/providers/grok-cli.ts +++ b/src/lib/oauth/providers/grok-cli.ts @@ -62,35 +62,44 @@ function extractTokenAndRefresh(input: unknown): { refreshToken: string | null; rawAuthJson: Record | null; } { + // Direct JWT string if (typeof input === "string") return { accessToken: input, refreshToken: null, rawAuthJson: null }; if (input && typeof input === "object") { - // auth.json format: first entry's "key" and "refresh_token" fields - const keys = Object.keys(input); - if ( - keys.length > 0 && - (input as Record)[keys[0]] && - typeof (input as Record)[keys[0]] === "object" - ) { - const entry = (input as Record>)[keys[0]]; - if (entry.key) { - return { - accessToken: String(entry.key), - refreshToken: typeof entry.refresh_token === "string" ? entry.refresh_token : null, - rawAuthJson: input as Record, - }; + const obj = input as Record; + + // The route handler wraps the token: { accessToken: }. + // Unwrap once before checking the inner value. + const inner = + typeof obj.accessToken === "object" && obj.accessToken !== null + ? (obj.accessToken as Record) + : obj; + + // auth.json format: { "https://auth.x.ai::...": { key: "eyJ...", refresh_token: "..." } } + if (inner && typeof inner === "object") { + const innerKeys = Object.keys(inner); + for (const k of innerKeys) { + const entry = inner[k]; + if (entry && typeof entry === "object" && "key" in entry) { + const e = entry as Record; + if (typeof e.key === "string" && e.key.startsWith("eyJ")) { + return { + accessToken: e.key, + refreshToken: typeof e.refresh_token === "string" ? e.refresh_token : null, + rawAuthJson: inner as Record, + }; + } + } } } - // Already has accessToken - if ((input as Record).accessToken) { + + // Raw JWT passed as { accessToken: "eyJ..." } + if (typeof obj.accessToken === "string" && obj.accessToken.startsWith("eyJ")) { return { - accessToken: String((input as Record).accessToken), - refreshToken: - typeof (input as Record).refreshToken === "string" - ? ((input as Record).refreshToken as string) - : null, - rawAuthJson: input as Record, + accessToken: obj.accessToken, + refreshToken: typeof obj.refreshToken === "string" ? obj.refreshToken : null, + rawAuthJson: null, }; } } From ae652e4412c790c695d19fe87e29e460e65693e8 Mon Sep 17 00:00:00 2001 From: vkonovalchuk Date: Mon, 29 Jun 2026 00:04:47 +0300 Subject: [PATCH 3/4] test(grok-cli): add unit tests for auth.json import and rawAuthJson Added tests covering: - Route-wrapped auth.json ({ accessToken: }) - Direct auth.json object with rawAuthJson population - Raw JWT string (no rawAuthJson) - Non-JWT accessToken string compatibility --- src/lib/oauth/providers/grok-cli.ts | 2 +- tests/unit/grok-cli-oauth.test.ts | 52 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/lib/oauth/providers/grok-cli.ts b/src/lib/oauth/providers/grok-cli.ts index 39ebb0448ee..7a333bd8cdf 100644 --- a/src/lib/oauth/providers/grok-cli.ts +++ b/src/lib/oauth/providers/grok-cli.ts @@ -95,7 +95,7 @@ function extractTokenAndRefresh(input: unknown): { } // Raw JWT passed as { accessToken: "eyJ..." } - if (typeof obj.accessToken === "string" && obj.accessToken.startsWith("eyJ")) { + if (typeof obj.accessToken === "string" && obj.accessToken.length > 0) { return { accessToken: obj.accessToken, refreshToken: typeof obj.refreshToken === "string" ? obj.refreshToken : null, diff --git a/tests/unit/grok-cli-oauth.test.ts b/tests/unit/grok-cli-oauth.test.ts index 16206ce8b05..723d4d7e99e 100644 --- a/tests/unit/grok-cli-oauth.test.ts +++ b/tests/unit/grok-cli-oauth.test.ts @@ -65,3 +65,55 @@ test("Grok Build OAuth Provider - mapTokens from object with accessToken", () => const result = grokCli.mapTokens(input, null); assert.equal(result.accessToken, "direct-token"); }); + +test("Grok Build OAuth Provider - mapTokens from route-wrapped auth.json", () => { + // The route handler wraps the token: { accessToken: }. + // This simulates what the import-token endpoint passes to mapTokens. + const authJson = { + "https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828": { + key: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature", + refresh_token: "test-refresh-token-wrapped", + expires_at: "2026-12-31T00:00:00Z", + }, + }; + const wrapped = { accessToken: authJson }; + const result = grokCli.mapTokens(wrapped, null); + + assert.ok( + result.accessToken.startsWith("eyJ"), + "accessToken should be JWT from wrapped auth.json" + ); + assert.equal(result.refreshToken, "test-refresh-token-wrapped"); + assert.equal(result.email, "test@example.com"); + assert.ok(result.providerSpecificData?.rawAuthJson, "rawAuthJson should be populated"); + assert.deepEqual( + result.providerSpecificData?.rawAuthJson, + authJson, + "rawAuthJson should equal the original auth.json" + ); +}); + +test("Grok Build OAuth Provider - mapTokens from direct auth.json has rawAuthJson", () => { + const authJson = { + "https://auth.x.ai::clientId": { + key: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature", + refresh_token: "direct-refresh", + }, + }; + const result = grokCli.mapTokens(authJson, null); + + assert.ok(result.accessToken.startsWith("eyJ")); + assert.equal(result.refreshToken, "direct-refresh"); + assert.deepEqual(result.providerSpecificData?.rawAuthJson, authJson); +}); + +test("Grok Build OAuth Provider - mapTokens from raw JWT has no rawAuthJson", () => { + const payload = { sub: "12345", email: "test@example.com" }; + const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64url"); + const mockJwt = `eyJhbGciOiJFUzI1NiJ9.${payloadBase64}.signature`; + const result = grokCli.mapTokens(mockJwt, null); + + assert.equal(result.accessToken, mockJwt); + assert.equal(result.refreshToken, null); + assert.equal(result.providerSpecificData?.rawAuthJson, undefined); +}); From c73a4b7514b643d3cb32b31045a18f955e600aa9 Mon Sep 17 00:00:00 2001 From: fulorgnas Date: Sun, 28 Jun 2026 19:18:34 -0300 Subject: [PATCH 4/4] fix(grok-cli): z.record needs explicit key type for current zod release/v3.8.40's zod requires z.record(keyType, valueType); the single-arg z.record(z.unknown()) form fails typecheck (TS2554). Pass z.string() as the key type for the auth.json object branch of the import-token schema. Co-authored-by: diegosouzapw --- src/shared/validation/schemas/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/validation/schemas/auth.ts b/src/shared/validation/schemas/auth.ts index 7ae8cbe8bb0..41ee0a8a7b0 100644 --- a/src/shared/validation/schemas/auth.ts +++ b/src/shared/validation/schemas/auth.ts @@ -133,7 +133,7 @@ export const oauthPollSchema = z.object({ /** Import a raw API token (e.g. WINDSURF_API_KEY) without going through the browser OAuth flow. */ export const oauthImportTokenSchema = z.object({ - token: z.union([z.string().trim().min(1, "Token is required"), z.record(z.unknown())]), + token: z.union([z.string().trim().min(1, "Token is required"), z.record(z.string(), z.unknown())]), connectionId: z.string().optional(), });