Skip to content

Commit a105641

Browse files
committed
fix(auth): address review feedback
1 parent 3c203f6 commit a105641

11 files changed

Lines changed: 167 additions & 37 deletions

packages/api/src/services/auth-account-counts.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ type HasCredentials = (
1313
const ignoredAuthAccountEntries: ReadonlySet<string> = new Set([".image"])
1414
const grokEnvApiKeyNames: ReadonlyArray<string> = ["GROK_DEPLOYMENT_KEY", "GROK_API_KEY", "XAI_API_KEY"]
1515

16+
const credentialCount = (connected: boolean): number => connected ? 1 : 0
17+
1618
const hasFileAtPath = (
1719
fs: FileSystem.FileSystem,
1820
filePath: string
@@ -235,16 +237,21 @@ export const countCodexCredentialAccounts = (
235237
return 0
236238
}
237239

238-
let count = yield* _(hasCodexAccountCredentials(fs, root), Effect.map((connected) => connected ? 1 : 0))
240+
let count = yield* _(
241+
hasCodexAccountCredentials(fs, root).pipe(
242+
Effect.orElseSucceed(() => false),
243+
Effect.map((connected) => credentialCount(connected))
244+
)
245+
)
239246
const entries = yield* _(fs.readDirectory(root))
240247
for (const entry of entries) {
241248
if (ignoredAuthAccountEntries.has(entry)) {
242249
continue
243250
}
244251

245252
const accountPath = path.join(root, entry)
246-
const info = yield* _(fs.stat(accountPath))
247-
if (info.type !== "Directory") {
253+
const info = yield* _(fs.stat(accountPath), Effect.orElseSucceed(() => null))
254+
if (info === null || info.type !== "Directory") {
248255
continue
249256
}
250257

@@ -268,16 +275,21 @@ export const countAuthCredentialAccounts = (
268275
return 0
269276
}
270277

278+
let count = yield* _(
279+
hasCredentials(fs, root).pipe(
280+
Effect.orElseSucceed(() => false),
281+
Effect.map((connected) => credentialCount(connected))
282+
)
283+
)
271284
const entries = yield* _(fs.readDirectory(root))
272-
let count = 0
273285
for (const entry of entries) {
274286
if (ignoredAuthAccountEntries.has(entry)) {
275287
continue
276288
}
277289

278290
const accountPath = path.join(root, entry)
279-
const info = yield* _(fs.stat(accountPath))
280-
if (info.type !== "Directory") {
291+
const info = yield* _(fs.stat(accountPath), Effect.orElseSucceed(() => null))
292+
if (info === null || info.type !== "Directory") {
281293
continue
282294
}
283295

packages/api/tests/auth-menu.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,24 @@ describe("auth menu service", () => {
6464
yield* _(fs.makeDirectory(path.join(authRoot, "codex", ".image"), { recursive: true }))
6565
yield* _(fs.makeDirectory(path.join(authRoot, "grok", ".image"), { recursive: true }))
6666

67+
yield* _(fs.writeFileString(path.join(authRoot, "claude", ".oauth-token"), "root-claude-oauth\n"))
6768
yield* _(fs.makeDirectory(path.join(authRoot, "claude", "live"), { recursive: true }))
6869
yield* _(fs.writeFileString(path.join(authRoot, "claude", "live", ".oauth-token"), "claude-oauth\n"))
6970
yield* _(fs.makeDirectory(path.join(authRoot, "codex"), { recursive: true }))
7071
yield* _(fs.writeFileString(path.join(authRoot, "codex", "auth.json"), "{\"tokens\":{\"account_id\":\"default\"}}\n"))
7172
yield* _(fs.makeDirectory(path.join(authRoot, "codex", "work"), { recursive: true }))
7273
yield* _(fs.writeFileString(path.join(authRoot, "codex", "work", "auth.json"), "{\"tokens\":{\"account_id\":\"work\"}}\n"))
74+
yield* _(fs.symlink(path.join(authRoot, "missing-gemini-account"), path.join(authRoot, "gemini", "broken")))
75+
yield* _(fs.writeFileString(path.join(authRoot, "gemini", ".api-key"), "root-gemini-key\n"))
7376
yield* _(fs.makeDirectory(path.join(authRoot, "gemini", "live"), { recursive: true }))
7477
yield* _(fs.writeFileString(path.join(authRoot, "gemini", "live", ".api-key"), "gemini-key\n"))
78+
yield* _(fs.makeDirectory(path.join(authRoot, "grok", ".grok"), { recursive: true }))
79+
yield* _(
80+
fs.writeFileString(
81+
path.join(authRoot, "grok", ".grok", "auth.json"),
82+
`${JSON.stringify({ [grokOidcAuthScope]: { key: "root-xai-oauth" } })}\n`
83+
)
84+
)
7585
yield* _(fs.makeDirectory(path.join(authRoot, "grok", "live", ".grok"), { recursive: true }))
7686
yield* _(
7787
fs.writeFileString(
@@ -93,10 +103,10 @@ describe("auth menu service", () => {
93103
)
94104
const snapshot = yield* _(withProjectsRoot(projectsRoot, service.readAuthMenuSnapshot()))
95105

96-
expect(snapshot.claudeAuthEntries).toBe(1)
106+
expect(snapshot.claudeAuthEntries).toBe(2)
97107
expect(snapshot.codexAuthEntries).toBe(2)
98-
expect(snapshot.geminiAuthEntries).toBe(1)
99-
expect(snapshot.grokAuthEntries).toBe(1)
108+
expect(snapshot.geminiAuthEntries).toBe(2)
109+
expect(snapshot.grokAuthEntries).toBe(2)
100110
})
101111
).pipe(Effect.provide(NodeContext.layer)))
102112
})

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ const decodeRequiredAuthSnapshot = (snapshot: RawAuthSnapshot): AuthSnapshot | n
7979
const requiredValues = [
8080
snapshot.globalEnvPath,
8181
snapshot.claudeAuthPath,
82+
snapshot.codexAuthPath,
8283
snapshot.geminiAuthPath,
8384
snapshot.grokAuthPath,
8485
snapshot.totalEntries,
8586
snapshot.githubTokenEntries,
8687
snapshot.gitTokenEntries,
8788
snapshot.gitUserEntries,
8889
snapshot.claudeAuthEntries,
90+
snapshot.codexAuthEntries,
8991
snapshot.geminiAuthEntries,
9092
snapshot.grokAuthEntries
9193
]

packages/app/src/docker-git/menu-auth-helpers.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type CredentialDirectoryCounterInput = {
1919

2020
const ignoredAuthAccountEntries: ReadonlySet<string> = new Set([".image"])
2121

22+
const credentialCount = (connected: boolean): number => connected ? 1 : 0
23+
2224
export const hasCodexAccountCredentials = (
2325
fs: FileSystem.FileSystem,
2426
accountPath: string
@@ -38,8 +40,8 @@ const countCredentialDirectories = ({
3840
continue
3941
}
4042
const accountPath = path.join(root, entry)
41-
const info = yield* _(fs.stat(accountPath))
42-
if (info.type !== "Directory") {
43+
const info = yield* _(fs.stat(accountPath), Effect.orElseSucceed(() => null))
44+
if (info === null || info.type !== "Directory") {
4345
continue
4446
}
4547
const connected = yield* _(hasCredentials(fs, accountPath), Effect.orElseSucceed(() => false))
@@ -54,11 +56,20 @@ const countExistingCredentialDirectories = (
5456
input: CredentialDirectoryCounterInput
5557
): Effect.Effect<number, PlatformError> =>
5658
input.fs.exists(input.root).pipe(
57-
Effect.flatMap((exists) =>
58-
exists
59-
? countCredentialDirectories(input)
60-
: Effect.succeed(0)
61-
)
59+
Effect.flatMap((exists) => {
60+
if (!exists) {
61+
return Effect.succeed(0)
62+
}
63+
return Effect.all({
64+
directoryCount: countCredentialDirectories(input),
65+
rootCount: input.hasCredentials(input.fs, input.root).pipe(
66+
Effect.orElseSucceed(() => false),
67+
Effect.map((connected) => credentialCount(connected))
68+
)
69+
}).pipe(
70+
Effect.map(({ directoryCount, rootCount }) => rootCount + directoryCount)
71+
)
72+
})
6273
)
6374

6475
export const countAuthCredentialAccounts = (
@@ -73,21 +84,4 @@ export const countCodexCredentialAccounts = (
7384
path: Path.Path,
7485
root: string
7586
): Effect.Effect<number, PlatformError> =>
76-
fs.exists(root).pipe(
77-
Effect.flatMap((exists) => {
78-
if (!exists) {
79-
return Effect.succeed(0)
80-
}
81-
return Effect.all({
82-
directoryCount: countCredentialDirectories({
83-
fs,
84-
hasCredentials: hasCodexAccountCredentials,
85-
path,
86-
root
87-
}),
88-
rootConnected: hasCodexAccountCredentials(fs, root).pipe(Effect.orElseSucceed(() => false))
89-
}).pipe(
90-
Effect.map(({ directoryCount, rootConnected }) => directoryCount + (rootConnected ? 1 : 0))
91-
)
92-
})
93-
)
87+
countExistingCredentialDirectories({ fs, hasCredentials: hasCodexAccountCredentials, path, root })

packages/app/src/web/actions-codex-oauth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const runCodexOauthMutation = (
2121
failureMessage: codexLoginFailureMessage,
2222
markers: codexLoginStreamMarkers,
2323
runStream: loginCodexStream,
24-
startMessage: "Codex OAuth запущен. Следуй инструкциям в Output.",
24+
startMessage: "Codex OAuth started. Follow the instructions in Output.",
2525
successMessage: (label) => `Saved Codex login (${label}).`,
2626
values
2727
})

packages/app/src/web/actions-github-oauth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const runGithubOauthMutation = (
1616
context.reloadDashboard()
1717
},
1818
runStream: loginGithubStream,
19-
startMessage: "GitHub OAuth запущен. Следуй инструкциям в Output.",
19+
startMessage: "GitHub OAuth started. Follow the instructions in Output.",
2020
successMessage: (label) => `Saved GitHub token (${label}).`,
2121
values
2222
})

packages/app/tests/docker-git/actions-codex-oauth.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ describe("web Codex OAuth action", () => {
5959

6060
runCodexOauthMutation({ label: "" }, context)
6161

62+
expect(setMessage).toHaveBeenNthCalledWith(
63+
1,
64+
"Codex OAuth started. Follow the instructions in Output."
65+
)
66+
6267
yield* _(waitForAssertion(() => {
6368
expect(context.setAuthSnapshot).toHaveBeenCalledWith(authSnapshot)
6469
}))

packages/app/tests/docker-git/actions-github-oauth.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ describe("web GitHub OAuth action", () => {
6767

6868
runGithubOauthMutation({ label: "" }, context)
6969

70+
expect(setMessage).toHaveBeenNthCalledWith(
71+
1,
72+
"GitHub OAuth started. Follow the instructions in Output."
73+
)
74+
7075
yield* _(waitForAssertion(() => {
7176
expect(reloadDashboard).toHaveBeenCalledTimes(1)
7277
}))
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it } from "@effect/vitest"
2+
3+
import { decodeAuthSnapshot } from "../../src/docker-git/api-auth-codec.js"
4+
import type { JsonValue } from "../../src/docker-git/api-json.js"
5+
6+
const fullAuthSnapshot = {
7+
claudeAuthEntries: 1,
8+
claudeAuthPath: "/auth/claude",
9+
codexAuthEntries: 2,
10+
codexAuthPath: "/auth/codex",
11+
geminiAuthEntries: 3,
12+
geminiAuthPath: "/auth/gemini",
13+
gitTokenEntries: 4,
14+
gitUserEntries: 5,
15+
githubTokenEntries: 6,
16+
globalEnvPath: "/env/global.env",
17+
grokAuthEntries: 7,
18+
grokAuthPath: "/auth/grok",
19+
totalEntries: 8
20+
} satisfies JsonValue
21+
22+
describe("api auth codec", () => {
23+
it("requires Codex fields in auth snapshots", () => {
24+
const missingPath = { ...fullAuthSnapshot, codexAuthPath: null } satisfies JsonValue
25+
const missingEntries = { ...fullAuthSnapshot, codexAuthEntries: null } satisfies JsonValue
26+
27+
expect(decodeAuthSnapshot({ snapshot: missingPath })).toBe(null)
28+
expect(decodeAuthSnapshot({ snapshot: missingEntries })).toBe(null)
29+
})
30+
31+
it("decodes complete auth snapshots", () => {
32+
expect(decodeAuthSnapshot({ snapshot: fullAuthSnapshot })).toEqual(fullAuthSnapshot)
33+
})
34+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* jscpd:ignore-start */
2+
import { NodeContext } from "@effect/platform-node"
3+
import type { PlatformError } from "@effect/platform/Error"
4+
import * as FileSystem from "@effect/platform/FileSystem"
5+
import * as Path from "@effect/platform/Path"
6+
import { describe, expect, it } from "@effect/vitest"
7+
import { Effect } from "effect"
8+
import type * as Scope from "effect/Scope"
9+
10+
import { countAuthCredentialAccounts, countCodexCredentialAccounts } from "../../src/docker-git/menu-auth-helpers.js"
11+
12+
const withTempDir = <A, E, R>(
13+
use: (tempDir: string) => Effect.Effect<A, E, R>
14+
): Effect.Effect<A, E | PlatformError, FileSystem.FileSystem | Exclude<R, Scope.Scope>> =>
15+
Effect.scoped(
16+
Effect.gen(function*(_) {
17+
const fs = yield* _(FileSystem.FileSystem)
18+
const tempDir = yield* _(
19+
fs.makeTempDirectoryScoped({
20+
prefix: "docker-git-auth-helpers-"
21+
})
22+
)
23+
return yield* _(use(tempDir))
24+
})
25+
)
26+
27+
const hasMarkerCredentials = (
28+
fs: FileSystem.FileSystem,
29+
accountPath: string
30+
): Effect.Effect<boolean, PlatformError> =>
31+
fs.readFileString(`${accountPath}/.token`).pipe(
32+
Effect.map((content) => content.trim().length > 0)
33+
)
34+
35+
describe("menu auth helpers", () => {
36+
it.effect("counts root credentials and skips broken directory entries", () =>
37+
withTempDir((root) =>
38+
Effect.gen(function*(_) {
39+
const fs = yield* _(FileSystem.FileSystem)
40+
const path = yield* _(Path.Path)
41+
yield* _(fs.makeDirectory(path.join(root, "work"), { recursive: true }))
42+
yield* _(fs.writeFileString(path.join(root, ".token"), "root-token\n"))
43+
yield* _(fs.writeFileString(path.join(root, "work", ".token"), "work-token\n"))
44+
yield* _(fs.symlink(path.join(root, "missing-account"), path.join(root, "broken")))
45+
46+
const count = yield* _(countAuthCredentialAccounts(fs, path, root, hasMarkerCredentials))
47+
48+
expect(count).toBe(2)
49+
})
50+
).pipe(Effect.provide(NodeContext.layer)))
51+
52+
it.effect("counts root and labeled Codex credentials", () =>
53+
withTempDir((root) =>
54+
Effect.gen(function*(_) {
55+
const fs = yield* _(FileSystem.FileSystem)
56+
const path = yield* _(Path.Path)
57+
yield* _(fs.makeDirectory(path.join(root, "work"), { recursive: true }))
58+
yield* _(fs.writeFileString(path.join(root, "auth.json"), "{}\n"))
59+
yield* _(fs.writeFileString(path.join(root, "work", "auth.json"), "{}\n"))
60+
yield* _(fs.symlink(path.join(root, "missing-account"), path.join(root, "broken")))
61+
62+
const count = yield* _(countCodexCredentialAccounts(fs, path, root))
63+
64+
expect(count).toBe(2)
65+
})
66+
).pipe(Effect.provide(NodeContext.layer)))
67+
})
68+
/* jscpd:ignore-end */

0 commit comments

Comments
 (0)