Skip to content

Commit e7107a6

Browse files
committed
Fix Grok auth flow and auth terminal cleanup
1 parent 6cdd919 commit e7107a6

17 files changed

Lines changed: 279 additions & 27 deletions

packages/api/src/api/contracts.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,18 @@ export type CodexAuthStatus = {
257257
readonly account: string | null
258258
}
259259

260+
export type GrokAuthStatus = {
261+
readonly label: string
262+
readonly message: string
263+
readonly connected: boolean
264+
readonly authPath: string
265+
readonly method: "none" | "api-key" | "oauth"
266+
}
267+
268+
export type GrokAuthLogoutRequest = {
269+
readonly label?: string | null | undefined
270+
}
271+
260272
export type CodexAuthLogoutRequest = {
261273
readonly label?: string | null | undefined
262274
}

packages/api/src/api/schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ export const CodexAuthLoginRequestSchema = Schema.Struct({
9999
label: OptionalNullableString
100100
})
101101

102+
export const GrokAuthLogoutRequestSchema = Schema.Struct({
103+
label: OptionalNullableString
104+
})
105+
102106
export const CodexAuthLogoutRequestSchema = Schema.Struct({
103107
label: OptionalNullableString
104108
})
@@ -345,6 +349,7 @@ export type GithubAuthLogoutRequestInput = Schema.Schema.Type<typeof GithubAuthL
345349
export type GitlabAuthLogoutRequestInput = Schema.Schema.Type<typeof GitlabAuthLogoutRequestSchema>
346350
export type CodexAuthImportRequestInput = Schema.Schema.Type<typeof CodexAuthImportRequestSchema>
347351
export type CodexAuthLoginRequestInput = Schema.Schema.Type<typeof CodexAuthLoginRequestSchema>
352+
export type GrokAuthLogoutRequestInput = Schema.Schema.Type<typeof GrokAuthLogoutRequestSchema>
348353
export type CodexAuthLogoutRequestInput = Schema.Schema.Type<typeof CodexAuthLogoutRequestSchema>
349354
export type ProjectAuthRequestInput = Schema.Schema.Type<typeof ProjectAuthRequestSchema>
350355
export type ProjectPromptKindInput = Schema.Schema.Type<typeof ProjectPromptKindSchema>

packages/api/src/http.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
ExchangeSubscribeRequestSchema,
2929
GitlabAuthLoginRequestSchema,
3030
GitlabAuthLogoutRequestSchema,
31+
GrokAuthLogoutRequestSchema,
3132
GithubAuthLoginRequestSchema,
3233
GithubAuthLogoutRequestSchema,
3334
ProjectDatabaseProfileRequestSchema,
@@ -50,9 +51,11 @@ import {
5051
loginGitlabAuth,
5152
loginGithubAuth,
5253
logoutCodexAuth,
54+
logoutGrokAuth,
5355
logoutGitlabAuth,
5456
logoutGithubAuth,
5557
readCodexAuthStatus,
58+
readGrokAuthStatus,
5659
readGitlabAuthStatus,
5760
readGithubAuthStatus,
5861
} from "./services/auth.js"
@@ -426,6 +429,7 @@ const readAuthMenuRequest = () => HttpServerRequest.schemaBodyJson(AuthMenuReque
426429
const readAuthTerminalSessionRequest = () => HttpServerRequest.schemaBodyJson(AuthTerminalSessionRequestSchema)
427430
const readCodexAuthImportRequest = () => HttpServerRequest.schemaBodyJson(CodexAuthImportRequestSchema)
428431
const readCodexAuthLoginRequest = () => HttpServerRequest.schemaBodyJson(CodexAuthLoginRequestSchema)
432+
const readGrokAuthLogoutRequest = () => HttpServerRequest.schemaBodyJson(GrokAuthLogoutRequestSchema)
429433
const readCodexAuthLogoutRequest = () => HttpServerRequest.schemaBodyJson(CodexAuthLogoutRequestSchema)
430434
const readProjectAuthRequest = () => HttpServerRequest.schemaBodyJson(ProjectAuthRequestSchema)
431435
const readProjectPromptUpdateRequest = () => HttpServerRequest.schemaBodyJson(ProjectPromptUpdateRequestSchema)
@@ -835,6 +839,15 @@ export const makeRouter = () => {
835839
return yield* _(jsonResponse({ status }, 200))
836840
}).pipe(Effect.catchAll(errorResponse))
837841
),
842+
HttpRouter.get(
843+
"/auth/grok/status",
844+
Effect.gen(function*(_) {
845+
const request = yield* _(HttpServerRequest.HttpServerRequest)
846+
const label = new URL(request.url, "http://localhost").searchParams.get("label")
847+
const status = yield* _(readGrokAuthStatus(label))
848+
return yield* _(jsonResponse({ status }, 200))
849+
}).pipe(Effect.catchAll(errorResponse))
850+
),
838851
HttpRouter.get(
839852
"/auth/menu",
840853
Effect.gen(function*(_) {
@@ -938,6 +951,14 @@ export const makeRouter = () => {
938951
return yield* _(jsonResponse({ ok: true, status }, 200))
939952
}).pipe(Effect.catchAll(errorResponse))
940953
),
954+
HttpRouter.post(
955+
"/auth/grok/logout",
956+
Effect.gen(function*(_) {
957+
const request = yield* _(readGrokAuthLogoutRequest())
958+
const status = yield* _(logoutGrokAuth(request))
959+
return yield* _(jsonResponse({ ok: true, status }, 200))
960+
}).pipe(Effect.catchAll(errorResponse))
961+
),
941962
HttpRouter.get(
942963
"/auth/codex/status",
943964
Effect.gen(function*(_) {

packages/api/src/services/auth.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { defaultTemplateConfig } from "@effect-template/lib/core/template-defaul
66
import { parseGithubRepoUrl, parseGitlabRepoUrl } from "@effect-template/lib/core/repo"
77
import { CommandFailedError } from "@effect-template/lib/shell/errors"
88
import { authCodexLogin as runCodexLogin } from "@effect-template/lib/usecases/auth-codex"
9+
import { authGrokLogout as runGrokLogout } from "@effect-template/lib/usecases/auth-grok-logout"
910
import { authGitlabLogin as runGitlabLogin, authGitlabLogout as runGitlabLogout, listGitlabTokens } from "@effect-template/lib/usecases/auth-gitlab"
1011
import { authGithubLogin as runGithubLogin, authGithubLogout as runGithubLogout } from "@effect-template/lib/usecases/auth-github"
1112
import { readEnvText } from "@effect-template/lib/usecases/env-file"
@@ -25,6 +26,7 @@ import {
2526
resolveGithubCloneAuthToken
2627
} from "@effect-template/lib/usecases/github-token-preflight"
2728
import { validateGithubToken, type GithubTokenValidationResult } from "@effect-template/lib/usecases/github-token-validation"
29+
import { resolveGrokAccountPath, resolveGrokAuthMethod } from "@effect-template/lib/usecases/auth-grok-helpers"
2830
import { normalizeAccountLabel } from "@effect-template/lib/usecases/auth-helpers"
2931
import { resolvePathFromCwd } from "@effect-template/lib/usecases/path-helpers"
3032
import { Effect, Logger, Match } from "effect"
@@ -34,6 +36,8 @@ import type {
3436
CodexAuthLoginRequest,
3537
CodexAuthLogoutRequest,
3638
CodexAuthStatus,
39+
GrokAuthLogoutRequest,
40+
GrokAuthStatus,
3741
GitlabAuthLoginRequest,
3842
GitlabAuthLogoutRequest,
3943
GitlabAuthStatus,
@@ -57,6 +61,7 @@ export const gitlabAuthRequiredMessage = [
5761
].join("\n")
5862
export const githubAuthEnvGlobalPath = defaultTemplateConfig.envGlobalPath
5963
export const codexAuthPath = defaultTemplateConfig.codexAuthPath
64+
export const grokAuthPath = defaultTemplateConfig.grokAuthPath
6065

6166
const githubTokenKey = "GITHUB_TOKEN"
6267
const githubTokenPrefix = "GITHUB_TOKEN__"
@@ -70,6 +75,7 @@ type GithubTokenEntry = {
7075
type JsonRecord = Readonly<Record<string, unknown>>
7176
type CodexRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
7277
type CodexCommandError = CommandFailedError | PlatformError
78+
type GrokCommandError = CommandFailedError | PlatformError
7379

7480
const labelFromKey = (key: string): string =>
7581
key.startsWith(githubTokenPrefix) ? key.slice(githubTokenPrefix.length) : "default"
@@ -138,6 +144,16 @@ const toCodexApiError = (error: CodexCommandError): ApiBadRequestError | ApiInte
138144
cause: error
139145
})
140146

147+
const toGrokApiError = (error: GrokCommandError): ApiBadRequestError | ApiInternalError =>
148+
error._tag === "CommandFailedError"
149+
? new ApiBadRequestError({
150+
message: `${error.command} failed (exit ${error.exitCode}).`
151+
})
152+
: new ApiInternalError({
153+
message: String(error),
154+
cause: error
155+
})
156+
141157
const runWithCapturedLogs = <R>(
142158
effect: Effect.Effect<void, CodexCommandError, R>,
143159
fallbackOutput: string
@@ -406,6 +422,32 @@ const codexAuthStatus = (
406422
account
407423
})
408424

425+
const grokAuthStatus = (
426+
label: string,
427+
authPath: string,
428+
method: GrokAuthStatus["method"]
429+
): GrokAuthStatus => ({
430+
label,
431+
message: method === "none"
432+
? `Grok not connected (${label}).`
433+
: `Grok connected (${label}, ${method}).`,
434+
connected: method !== "none",
435+
authPath,
436+
method
437+
})
438+
439+
export const readGrokAuthStatus = (
440+
label?: string | null | undefined
441+
): Effect.Effect<GrokAuthStatus, PlatformError, FileSystem.FileSystem | Path.Path> =>
442+
Effect.gen(function*(_) {
443+
const fs = yield* _(FileSystem.FileSystem)
444+
const path = yield* _(Path.Path)
445+
const rootPath = resolvePathFromCwd(path, process.cwd(), grokAuthPath)
446+
const { accountLabel, accountPath } = resolveGrokAccountPath(path, rootPath, label ?? null)
447+
const method = yield* _(resolveGrokAuthMethod(fs, accountPath))
448+
return grokAuthStatus(accountLabel, accountPath, method)
449+
})
450+
409451
export const readCodexAuthStatus = (
410452
label?: string | null | undefined
411453
): Effect.Effect<CodexAuthStatus, PlatformError, FileSystem.FileSystem | Path.Path> =>
@@ -489,6 +531,25 @@ export const logoutCodexAuth = (
489531
return yield* _(readCodexAuthStatus(request.label))
490532
})
491533

534+
export const logoutGrokAuth = (
535+
request: GrokAuthLogoutRequest
536+
): Effect.Effect<
537+
GrokAuthStatus,
538+
PlatformError | ApiBadRequestError | ApiInternalError,
539+
FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
540+
> =>
541+
Effect.gen(function*(_) {
542+
yield* _(
543+
runGrokLogout({
544+
_tag: "AuthGrokLogout",
545+
label: request.label ?? null,
546+
grokAuthPath
547+
}).pipe(Effect.mapError(toGrokApiError))
548+
)
549+
550+
return yield* _(readGrokAuthStatus(request.label))
551+
})
552+
492553
export const ensureGithubAuthForCreate = (config: {
493554
readonly repoUrl: string
494555
readonly gitTokenLabel?: string | undefined

packages/api/tests/auth.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
ensureGitlabAuthForCreate,
1717
importCodexAuth,
1818
logoutCodexAuth,
19+
logoutGrokAuth,
1920
readCodexAuthStatus,
21+
readGrokAuthStatus,
2022
readGitlabAuthStatus,
2123
readGithubAuthStatus
2224
} from "../src/services/auth.js"
@@ -482,4 +484,57 @@ describe("api auth", () => {
482484
expect(removed.authPath).toBe(path.join(labeledAuthDir, "auth.json"))
483485
})
484486
).pipe(Effect.provide(NodeContext.layer)))
487+
488+
it.effect("reads labeled Grok auth status from controller state", () =>
489+
withTempDir((root) =>
490+
Effect.gen(function*(_) {
491+
const fs = yield* _(FileSystem.FileSystem)
492+
const path = yield* _(Path.Path)
493+
const projectsRoot = path.join(root, ".docker-git")
494+
const accountDir = path.join(projectsRoot, ".orch", "auth", "grok", "team-a")
495+
496+
yield* _(fs.makeDirectory(accountDir, { recursive: true }))
497+
yield* _(fs.writeFileString(path.join(accountDir, ".api-key"), "xai-test-key\n"))
498+
499+
const status = yield* _(
500+
withProjectsRoot(
501+
projectsRoot,
502+
withWorkingDirectory(root, readGrokAuthStatus("team-a"))
503+
)
504+
)
505+
506+
expect(status.connected).toBe(true)
507+
expect(status.label).toBe("team-a")
508+
expect(status.method).toBe("api-key")
509+
expect(status.authPath).toBe(accountDir)
510+
expect(status.message).toBe("Grok connected (team-a, api-key).")
511+
})
512+
).pipe(Effect.provide(NodeContext.layer)))
513+
514+
it.effect("removes labeled Grok auth from controller state", () =>
515+
withTempDir((root) =>
516+
Effect.gen(function*(_) {
517+
const fs = yield* _(FileSystem.FileSystem)
518+
const path = yield* _(Path.Path)
519+
const projectsRoot = path.join(root, ".docker-git")
520+
const accountDir = path.join(projectsRoot, ".orch", "auth", "grok", "team-a")
521+
522+
yield* _(fs.makeDirectory(path.join(accountDir, ".grok"), { recursive: true }))
523+
yield* _(fs.writeFileString(path.join(accountDir, ".api-key"), "xai-test-key\n"))
524+
yield* _(fs.writeFileString(path.join(accountDir, ".grok", "user-settings.json"), "{\"apiKey\":\"xai-test-key\"}\n"))
525+
526+
const status = yield* _(
527+
withProjectsRoot(
528+
projectsRoot,
529+
withWorkingDirectory(root, logoutGrokAuth({ label: "team-a" }))
530+
)
531+
)
532+
533+
expect(status.connected).toBe(false)
534+
expect(status.label).toBe("team-a")
535+
expect(status.method).toBe("none")
536+
expect(status.authPath).toBe(accountDir)
537+
expect(status.message).toBe("Grok not connected (team-a).")
538+
})
539+
).pipe(Effect.provide(NodeContext.layer)))
485540
})

packages/app/src/docker-git/api-client-auth.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import type {
2525
AuthGithubLoginCommand,
2626
AuthGithubLogoutCommand,
2727
AuthGithubStatusCommand,
28+
AuthGrokLogoutCommand,
29+
AuthGrokStatusCommand,
2830
AuthGitlabLoginCommand,
2931
AuthGitlabLogoutCommand,
3032
AuthGitlabStatusCommand
@@ -193,6 +195,16 @@ export const codexStatus = (command: AuthCodexStatusCommand) => {
193195
return request("GET", `/auth/codex/status${query}`)
194196
}
195197

198+
export const grokStatus = (command: AuthGrokStatusCommand) => {
199+
const query = command.label === null ? "" : `?label=${encodeURIComponent(command.label)}`
200+
return request("GET", `/auth/grok/status${query}`)
201+
}
202+
203+
export const grokLogout = (command: AuthGrokLogoutCommand) =>
204+
requestVoid("POST", "/auth/grok/logout", {
205+
label: command.label
206+
})
207+
196208
export const codexLogout = (command: AuthCodexLogoutCommand) =>
197209
requestVoid("POST", "/auth/codex/logout", {
198210
label: command.label

packages/app/src/docker-git/api-client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export {
3131
githubLogin,
3232
githubLogout,
3333
githubStatus,
34+
grokLogout,
35+
grokStatus,
3436
gitlabLogin,
3537
gitlabLogout,
3638
gitlabStatus

0 commit comments

Comments
 (0)