Skip to content

Commit 83eb61f

Browse files
thdxrOpenCode
andcommitted
Refactor authentication system to consolidate auth flow and remove provider-based commands
🤖 Generated with [OpenCode](https://opencode.ai) Co-Authored-By: OpenCode <noreply@opencode.ai>
1 parent b8e7d06 commit 83eb61f

File tree

12 files changed

+308
-235
lines changed

12 files changed

+308
-235
lines changed

packages/opencode/src/app/app.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export namespace App {
3535
async function create(input: { cwd: string; version: string }) {
3636
log.info("creating", {
3737
cwd: input.cwd,
38-
version: input.version,
3938
})
4039
const git = await Filesystem.findUp(".git", input.cwd).then(([x]) =>
4140
x ? path.dirname(x) : undefined,

packages/opencode/src/auth/anthropic.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import { generatePKCE } from "@openauthjs/openauth/pkce"
2-
import { Global } from "../global"
3-
import path from "path"
42
import fs from "fs/promises"
3+
import { Auth } from "./index"
54

65
export namespace AuthAnthropic {
76
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
87

9-
const filepath = path.join(Global.Path.data, "auth", "anthropic.json")
10-
118
export async function authorize() {
129
const pkce = await generatePKCE()
1310
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
@@ -48,16 +45,17 @@ export namespace AuthAnthropic {
4845
}),
4946
})
5047
if (!result.ok) throw new ExchangeFailed()
51-
const file = Bun.file(filepath)
52-
await Bun.write(file, result)
53-
await fs.chmod(file.name!, 0o600)
48+
const json = await result.json()
49+
await Auth.set("anthropic", {
50+
type: "oauth",
51+
refresh: json.refresh_token as string,
52+
expires: Date.now() + json.expires_in * 1000,
53+
})
5454
}
5555

5656
export async function access() {
57-
const file = Bun.file(filepath)
58-
const result = await file.json().catch(() => ({}))
59-
if (!result) return
60-
const refresh = result.refresh_token
57+
const info = await Auth.get("anthropic")
58+
if (!info || info.type !== "oauth") return
6159
const response = await fetch(
6260
"https://console.anthropic.com/v1/oauth/token",
6361
{
@@ -67,14 +65,18 @@ export namespace AuthAnthropic {
6765
},
6866
body: JSON.stringify({
6967
grant_type: "refresh_token",
70-
refresh_token: refresh,
68+
refresh_token: info.refresh,
7169
client_id: CLIENT_ID,
7270
}),
7371
},
7472
)
7573
if (!response.ok) return
7674
const json = await response.json()
77-
await Bun.write(file, JSON.stringify(json))
75+
await Auth.set("anthropic", {
76+
type: "oauth",
77+
refresh: json.refresh_token as string,
78+
expires: Date.now() + json.expires_in * 1000,
79+
})
7880
return json.access_token as string
7981
}
8082

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import path from "path"
2+
import { Global } from "../global"
3+
import fs from "fs/promises"
4+
import { z } from "zod"
5+
6+
export namespace Auth {
7+
export const Oauth = z.object({
8+
type: z.literal("oauth"),
9+
refresh: z.string(),
10+
expires: z.number(),
11+
})
12+
13+
export const Api = z.object({
14+
type: z.literal("api"),
15+
key: z.string(),
16+
})
17+
18+
export const Info = z.discriminatedUnion("type", [Oauth, Api])
19+
export type Info = z.infer<typeof Info>
20+
21+
const filepath = path.join(Global.Path.data, "auth.json")
22+
23+
export async function get(providerID: string) {
24+
const file = Bun.file(filepath)
25+
return file
26+
.json()
27+
.catch(() => ({}))
28+
.then((x) => x[providerID] as Info | undefined)
29+
}
30+
31+
export async function all(): Promise<Record<string, Info>> {
32+
const file = Bun.file(filepath)
33+
return file.json().catch(() => ({}))
34+
}
35+
36+
export async function set(key: string, info: Info) {
37+
const file = Bun.file(filepath)
38+
const data = await all()
39+
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
40+
await fs.chmod(file.name!, 0o600)
41+
}
42+
43+
export async function remove(key: string) {
44+
const file = Bun.file(filepath)
45+
const data = await all()
46+
delete data[key]
47+
await Bun.write(file, JSON.stringify(data, null, 2))
48+
await fs.chmod(file.name!, 0o600)
49+
}
50+
}

packages/opencode/src/auth/keys.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { AuthAnthropic } from "../../auth/anthropic"
2+
import { Auth } from "../../auth"
3+
import { cmd } from "./cmd"
4+
import * as prompts from "@clack/prompts"
5+
import open from "open"
6+
import { UI } from "../ui"
7+
import { ModelsDev } from "../../provider/models"
8+
9+
export const AuthCommand = cmd({
10+
command: "auth",
11+
builder: (yargs) =>
12+
yargs
13+
.command(AuthLoginCommand)
14+
.command(AuthLogoutCommand)
15+
.command(AuthListCommand)
16+
.demandCommand(),
17+
async handler(args) {},
18+
})
19+
20+
export const AuthListCommand = cmd({
21+
command: "list",
22+
aliases: ["ls"],
23+
describe: "list providers",
24+
async handler() {
25+
UI.empty()
26+
prompts.intro("Credentials")
27+
const results = await Auth.all().then((x) => Object.entries(x))
28+
const database = await ModelsDev.get()
29+
30+
for (const [providerID, result] of results) {
31+
const name = database[providerID]?.name || providerID
32+
prompts.log.info(`${name} ${Bun.color("gray", "ansi")}(${result.type})`)
33+
}
34+
35+
prompts.outro(`${results.length} credentials`)
36+
},
37+
})
38+
39+
export const AuthLoginCommand = cmd({
40+
command: "login",
41+
describe: "login to a provider",
42+
async handler() {
43+
UI.empty()
44+
prompts.intro("Add credential")
45+
const provider = await prompts.select({
46+
message: "Select provider",
47+
maxItems: 2,
48+
options: [
49+
{
50+
label: "Anthropic",
51+
value: "anthropic",
52+
},
53+
{
54+
label: "OpenAI",
55+
value: "openai",
56+
},
57+
{
58+
label: "Google",
59+
value: "google",
60+
},
61+
],
62+
})
63+
if (prompts.isCancel(provider)) throw new UI.CancelledError()
64+
65+
if (provider === "anthropic") {
66+
const method = await prompts.select({
67+
message: "Login method",
68+
options: [
69+
{
70+
label: "Claude Pro/Max",
71+
value: "oauth",
72+
},
73+
{
74+
label: "API Key",
75+
value: "api",
76+
},
77+
],
78+
})
79+
if (prompts.isCancel(method)) throw new UI.CancelledError()
80+
81+
if (method === "oauth") {
82+
// some weird bug where program exits without this
83+
await new Promise((resolve) => setTimeout(resolve, 10))
84+
const { url, verifier } = await AuthAnthropic.authorize()
85+
prompts.note("Opening browser...")
86+
await open(url)
87+
prompts.log.info(url)
88+
89+
const code = await prompts.text({
90+
message: "Paste the authorization code here: ",
91+
validate: (x) => (x.length > 0 ? undefined : "Required"),
92+
})
93+
if (prompts.isCancel(code)) throw new UI.CancelledError()
94+
95+
await AuthAnthropic.exchange(code, verifier)
96+
.then(() => {
97+
prompts.log.success("Login successful")
98+
})
99+
.catch(() => {
100+
prompts.log.error("Invalid code")
101+
})
102+
prompts.outro("Done")
103+
return
104+
}
105+
}
106+
107+
const key = await prompts.password({
108+
message: "Enter your API key",
109+
validate: (x) => (x.length > 0 ? undefined : "Required"),
110+
})
111+
if (prompts.isCancel(key)) throw new UI.CancelledError()
112+
await Auth.set(provider, {
113+
type: "api",
114+
key,
115+
})
116+
117+
prompts.outro("Done")
118+
},
119+
})
120+
121+
export const AuthLogoutCommand = cmd({
122+
command: "logout",
123+
describe: "logout from a configured provider",
124+
async handler() {
125+
UI.empty()
126+
const credentials = await Auth.all().then((x) => Object.entries(x))
127+
prompts.intro("Remove credential")
128+
if (credentials.length === 0) {
129+
prompts.log.error("No credentials found")
130+
return
131+
}
132+
const database = await ModelsDev.get()
133+
const providerID = await prompts.select({
134+
message: "Select credential",
135+
options: credentials.map(([key, value]) => ({
136+
label: database[key]?.name || key,
137+
value: key,
138+
})),
139+
})
140+
if (prompts.isCancel(providerID)) throw new UI.CancelledError()
141+
await Auth.remove(providerID)
142+
prompts.outro("Logout successful")
143+
},
144+
})

0 commit comments

Comments
 (0)