Skip to content

Commit 41a0391

Browse files
committed
fix(auth): switch Claude OAuth to setup-token flow
1 parent 4df6b11 commit 41a0391

File tree

6 files changed

+257
-288
lines changed

6 files changed

+257
-288
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { PlatformError } from "@effect/platform/Error"
2+
import type * as FileSystem from "@effect/platform/FileSystem"
3+
import { Effect } from "effect"
4+
5+
const oauthTokenFileName = ".oauth-token"
6+
const legacyConfigFileName = ".config.json"
7+
8+
const hasFileAtPath = (
9+
fs: FileSystem.FileSystem,
10+
filePath: string
11+
): Effect.Effect<boolean, PlatformError> =>
12+
Effect.gen(function*(_) {
13+
const exists = yield* _(fs.exists(filePath))
14+
if (!exists) {
15+
return false
16+
}
17+
const info = yield* _(fs.stat(filePath))
18+
return info.type === "File"
19+
})
20+
21+
const hasNonEmptyOauthToken = (
22+
fs: FileSystem.FileSystem,
23+
tokenPath: string
24+
): Effect.Effect<boolean, PlatformError> =>
25+
Effect.gen(function*(_) {
26+
const hasFile = yield* _(hasFileAtPath(fs, tokenPath))
27+
if (!hasFile) {
28+
return false
29+
}
30+
const tokenValue = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""))
31+
return tokenValue.trim().length > 0
32+
})
33+
34+
const hasLegacyClaudeAuthFile = (
35+
fs: FileSystem.FileSystem,
36+
accountPath: string
37+
): Effect.Effect<boolean, PlatformError> =>
38+
Effect.gen(function*(_) {
39+
const entries = yield* _(fs.readDirectory(accountPath))
40+
for (const entry of entries) {
41+
if (!entry.startsWith(".claude") || !entry.endsWith(".json")) {
42+
continue
43+
}
44+
const isFile = yield* _(hasFileAtPath(fs, `${accountPath}/${entry}`))
45+
if (isFile) {
46+
return true
47+
}
48+
}
49+
return false
50+
})
51+
52+
export const hasClaudeAccountCredentials = (
53+
fs: FileSystem.FileSystem,
54+
accountPath: string
55+
): Effect.Effect<boolean, PlatformError> =>
56+
hasFileAtPath(fs, `${accountPath}/${legacyConfigFileName}`).pipe(
57+
Effect.flatMap((hasConfig) => {
58+
if (hasConfig) {
59+
return Effect.succeed(true)
60+
}
61+
return hasNonEmptyOauthToken(fs, `${accountPath}/${oauthTokenFileName}`).pipe(
62+
Effect.flatMap((hasOauthToken) => {
63+
if (hasOauthToken) {
64+
return Effect.succeed(true)
65+
}
66+
return hasLegacyClaudeAuthFile(fs, accountPath)
67+
})
68+
)
69+
})
70+
)

packages/app/src/docker-git/menu-project-auth-data.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { autoSyncState } from "@effect-template/lib/usecases/state-repo"
1212

1313
import { countAuthAccountDirectories } from "./menu-auth-helpers.js"
1414
import { buildLabeledEnvKey, countKeyEntries, normalizeLabel } from "./menu-labeled-env.js"
15+
import { hasClaudeAccountCredentials } from "./menu-project-auth-claude.js"
1516
import type { MenuEnv, ProjectAuthFlow, ProjectAuthSnapshot } from "./menu-types.js"
1617

1718
export type ProjectAuthMenuAction = ProjectAuthFlow | "Refresh" | "Back"
@@ -202,24 +203,13 @@ const updateProjectClaudeConnect = (spec: ProjectEnvUpdateSpec): Effect.Effect<s
202203
if (!exists) {
203204
return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)))
204205
}
205-
const configJsonPath = `${accountPath}/.config.json`
206-
const hasConfigJson = yield* _(spec.fs.exists(configJsonPath))
207-
if (hasConfigJson) {
208-
const info = yield* _(spec.fs.stat(configJsonPath))
209-
if (info.type === "File") {
210-
return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel)
211-
}
212-
}
213206

214-
const entries = yield* _(spec.fs.readDirectory(accountPath))
215-
for (const entry of entries) {
216-
if (!entry.startsWith(".claude") || !entry.endsWith(".json")) {
217-
continue
218-
}
219-
const info = yield* _(spec.fs.stat(`${accountPath}/${entry}`))
220-
if (info.type === "File") {
221-
return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel)
222-
}
207+
const hasCredentials = yield* _(
208+
hasClaudeAccountCredentials(spec.fs, accountPath),
209+
Effect.orElseSucceed(() => false)
210+
)
211+
if (hasCredentials) {
212+
return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel)
223213
}
224214

225215
return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)))

packages/lib/src/core/templates-entrypoint/claude.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,21 @@ export CLAUDE_CONFIG_DIR
2323
2424
mkdir -p "$CLAUDE_CONFIG_DIR" || true
2525
26+
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
27+
CLAUDE_CODE_OAUTH_TOKEN=""
28+
if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
29+
CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
30+
fi
31+
export CLAUDE_CODE_OAUTH_TOKEN
32+
2633
CLAUDE_PROFILE="/etc/profile.d/claude-config.sh"
2734
printf "export CLAUDE_AUTH_LABEL=%q\n" "$CLAUDE_AUTH_LABEL" > "$CLAUDE_PROFILE"
2835
printf "export CLAUDE_CONFIG_DIR=%q\n" "$CLAUDE_CONFIG_DIR" >> "$CLAUDE_PROFILE"
36+
if [[ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]]; then
37+
printf "export CLAUDE_CODE_OAUTH_TOKEN=%q\n" "$CLAUDE_CODE_OAUTH_TOKEN" >> "$CLAUDE_PROFILE"
38+
fi
2939
chmod 0644 "$CLAUDE_PROFILE" || true
3040
3141
docker_git_upsert_ssh_env "CLAUDE_AUTH_LABEL" "$CLAUDE_AUTH_LABEL"
32-
docker_git_upsert_ssh_env "CLAUDE_CONFIG_DIR" "$CLAUDE_CONFIG_DIR"`
42+
docker_git_upsert_ssh_env "CLAUDE_CONFIG_DIR" "$CLAUDE_CONFIG_DIR"
43+
docker_git_upsert_ssh_env "CLAUDE_CODE_OAUTH_TOKEN" "$CLAUDE_CODE_OAUTH_TOKEN"`

packages/lib/src/usecases/auth-claude-oauth-input.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)