Skip to content

Commit 199ef93

Browse files
saravmajesticclaude
andcommitted
fix(auth): auth logout removes the saved Altimate gateway key
`altimate-code auth logout` for `altimate-backend` only dropped the `auth.json` entry. Browser sign-in also writes the key to `~/.altimate/altimate.json`, which the provider loader and every workspace call read first, so the signed-out user's key kept working. - Call `AltimateApi.clearCredentials()` when logging out of `altimate-backend` - List `altimate-backend` in the logout picker when only `altimate.json` exists (TUI paste dialog), so it can be signed out from the CLI - TUI `/logout` also removes the `auth.json` entry, so a pasted `url::instance::key` no longer keeps the provider loader's fallback live - Test both the browser sign-in and file-only cases Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 011b742 commit 199ef93

3 files changed

Lines changed: 81 additions & 0 deletions

File tree

‎packages/opencode/src/cli/cmd/providers.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { Hooks } from "@opencode-ai/plugin"
1919
import { Process } from "@/util/process"
2020
import { errorMessage } from "@/util/error"
2121
import { FreeTier } from "@/altimate/free/client"
22+
import { AltimateApi } from "@/altimate/api/client"
2223
import { text } from "node:stream/consumers"
2324
import { Effect, Option } from "effect"
2425

@@ -605,6 +606,14 @@ export const ProvidersLogoutCommand = effectCmd({
605606
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
606607
value: key,
607608
}))
609+
// Credentials saved only to ~/.altimate/altimate.json (TUI paste dialog) have no auth-store entry.
610+
const hasAltimateBackendFile = yield* Effect.promise(() => AltimateApi.isConfigured())
611+
if (hasAltimateBackendFile && !options.some((option) => option.value === "altimate-backend")) {
612+
options.push({
613+
label: (database["altimate-backend"]?.name || "altimate-backend") + UI.Style.TEXT_DIM + " (api)",
614+
value: "altimate-backend",
615+
})
616+
}
608617
if (hasAltimateBaseCredential || hasLegacyAltimateBaseCredential || hasAltimateBaseState) {
609618
options.push({
610619
label: "Altimate Base" + UI.Style.TEXT_DIM + " (managed)",
@@ -638,6 +647,11 @@ export const ProvidersLogoutCommand = effectCmd({
638647
yield* Prompt.outro("Logout successful")
639648
return
640649
}
650+
if (provider === "altimate-backend") {
651+
// Browser sign-in persists the gateway key to ~/.altimate/altimate.json, which the provider
652+
// loader and every workspace call read first, so sign-out must remove it too.
653+
yield* cliTry("Failed to remove Altimate credential: ", () => AltimateApi.clearCredentials())
654+
}
641655
// altimate_change end
642656
yield* Effect.orDie(authSvc.remove(provider))
643657
yield* Prompt.outro("Logout successful")

‎packages/opencode/src/plugin/tui/altimate/provider-credentials.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ function show(api: TuiPluginApi) {
8787
async function logout(api: TuiPluginApi) {
8888
try {
8989
await AltimateApi.clearCredentials()
90+
// A pasted url::instance::key in the auth store keeps the provider loader's fallback path live.
91+
await api.client.auth.remove({ providerID: PROVIDER_ID })
9092
await api.client.instance.dispose()
9193
api.ui.toast({ variant: "success", message: "Signed out of Altimate LLM Gateway" })
9294
} catch (err) {

‎packages/opencode/test/cli/providers-logout.test.ts‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,69 @@ describe("providers logout", () => {
9797
}),
9898
120_000,
9999
)
100+
101+
cliIt.live(
102+
"removes the saved Altimate gateway key along with the auth-store entry",
103+
({ home, opencode }) =>
104+
Effect.gen(function* () {
105+
const dataDir = path.join(home, ".local", "share", "altimate-code")
106+
const authPath = path.join(dataDir, "auth.json")
107+
const altimatePath = path.join(home, ".altimate", "altimate.json")
108+
const anthropic = { type: "api", key: "anthropic-test-key" }
109+
const writeAltimate = () =>
110+
fs.writeFile(
111+
altimatePath,
112+
JSON.stringify({
113+
altimateUrl: "https://api.test",
114+
altimateInstanceName: "tenant",
115+
altimateApiKey: "gateway-test-key",
116+
}),
117+
{ mode: 0o600 },
118+
)
119+
120+
yield* Effect.promise(() => fs.mkdir(dataDir, { recursive: true }))
121+
yield* Effect.promise(() => fs.mkdir(path.dirname(altimatePath), { recursive: true }))
122+
yield* Effect.promise(() => writeAltimate())
123+
yield* Effect.promise(() =>
124+
fs.writeFile(
125+
authPath,
126+
JSON.stringify({ anthropic, "altimate-backend": { type: "api", key: "gateway-test-key" } }),
127+
{ mode: 0o600 },
128+
),
129+
)
130+
131+
const logout = yield* opencode.spawn(["providers", "logout", "altimate-backend"], {
132+
env: { OPENCODE_AUTH_CONTENT: "" },
133+
})
134+
opencode.expectExit(logout, 0, "providers logout altimate-backend")
135+
expect(logout.stdout).toContain("Logout successful")
136+
expect(
137+
yield* Effect.promise(() =>
138+
fs.stat(altimatePath).then(
139+
() => true,
140+
() => false,
141+
),
142+
),
143+
).toBe(false)
144+
expect(JSON.parse(yield* Effect.promise(() => fs.readFile(authPath, "utf8")))).toEqual({ anthropic })
145+
146+
// Credentials saved only to altimate.json (TUI paste dialog) must still be removable.
147+
yield* Effect.promise(() => writeAltimate())
148+
const fileOnlyLogout = yield* opencode.spawn(["providers", "logout", "altimate-backend"], {
149+
env: { OPENCODE_AUTH_CONTENT: "" },
150+
})
151+
opencode.expectExit(fileOnlyLogout, 0, "providers logout file-only altimate-backend")
152+
expect(fileOnlyLogout.stdout).toContain("Logout successful")
153+
expect(
154+
yield* Effect.promise(() =>
155+
fs.stat(altimatePath).then(
156+
() => true,
157+
() => false,
158+
),
159+
),
160+
).toBe(false)
161+
expect(JSON.parse(yield* Effect.promise(() => fs.readFile(authPath, "utf8")))).toEqual({ anthropic })
162+
}),
163+
120_000,
164+
)
100165
})

0 commit comments

Comments
 (0)