Skip to content

Commit b5b644c

Browse files
Apply PR #17212: refactor(auth): effectify AuthService
2 parents 8226c33 + 1739817 commit b5b644c

3 files changed

Lines changed: 116 additions & 30 deletions

File tree

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import path from "path"
2-
import { Global } from "../global"
1+
import { Effect } from "effect"
32
import z from "zod"
4-
import { Filesystem } from "../util/filesystem"
3+
import { runtime } from "@/effect/runtime"
4+
import * as S from "./service"
55

6-
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
6+
export { OAUTH_DUMMY_KEY } from "./service"
7+
8+
function runPromise<A>(f: (service: S.AuthService.Service) => Effect.Effect<A, S.AuthServiceError>) {
9+
return runtime.runPromise(S.AuthService.use(f))
10+
}
711

812
export namespace Auth {
913
export const Oauth = z
@@ -35,39 +39,19 @@ export namespace Auth {
3539
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" })
3640
export type Info = z.infer<typeof Info>
3741

38-
const filepath = path.join(Global.Path.data, "auth.json")
39-
4042
export async function get(providerID: string) {
41-
const auth = await all()
42-
return auth[providerID]
43+
return runPromise((service) => service.get(providerID))
4344
}
4445

4546
export async function all(): Promise<Record<string, Info>> {
46-
const data = await Filesystem.readJson<Record<string, unknown>>(filepath).catch(() => ({}))
47-
return Object.entries(data).reduce(
48-
(acc, [key, value]) => {
49-
const parsed = Info.safeParse(value)
50-
if (!parsed.success) return acc
51-
acc[key] = parsed.data
52-
return acc
53-
},
54-
{} as Record<string, Info>,
55-
)
47+
return runPromise((service) => service.all())
5648
}
5749

5850
export async function set(key: string, info: Info) {
59-
const normalized = key.replace(/\/+$/, "")
60-
const data = await all()
61-
if (normalized !== key) delete data[key]
62-
delete data[normalized + "/"]
63-
await Filesystem.writeJson(filepath, { ...data, [normalized]: info }, 0o600)
51+
return runPromise((service) => service.set(key, info))
6452
}
6553

6654
export async function remove(key: string) {
67-
const normalized = key.replace(/\/+$/, "")
68-
const data = await all()
69-
delete data[key]
70-
delete data[normalized]
71-
await Filesystem.writeJson(filepath, data, 0o600)
55+
return runPromise((service) => service.remove(key))
7256
}
7357
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import path from "path"
2+
import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
3+
import { Global } from "../global"
4+
import { Filesystem } from "../util/filesystem"
5+
6+
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
7+
8+
export class Oauth extends Schema.Class<Oauth>("OAuth")({
9+
type: Schema.Literal("oauth"),
10+
refresh: Schema.String,
11+
access: Schema.String,
12+
expires: Schema.Number,
13+
accountId: Schema.optional(Schema.String),
14+
enterpriseUrl: Schema.optional(Schema.String),
15+
}) {}
16+
17+
export class Api extends Schema.Class<Api>("ApiAuth")({
18+
type: Schema.Literal("api"),
19+
key: Schema.String,
20+
}) {}
21+
22+
export class WellKnown extends Schema.Class<WellKnown>("WellKnownAuth")({
23+
type: Schema.Literal("wellknown"),
24+
key: Schema.String,
25+
token: Schema.String,
26+
}) {}
27+
28+
export const Info = Schema.Union([Oauth, Api, WellKnown])
29+
export type Info = Schema.Schema.Type<typeof Info>
30+
31+
export class AuthServiceError extends Schema.TaggedErrorClass<AuthServiceError>()("AuthServiceError", {
32+
message: Schema.String,
33+
cause: Schema.optional(Schema.Defect),
34+
}) {}
35+
36+
const file = path.join(Global.Path.data, "auth.json")
37+
38+
const fail = (message: string) => (cause: unknown) => new AuthServiceError({ message, cause })
39+
40+
export namespace AuthService {
41+
export interface Service {
42+
readonly get: (providerID: string) => Effect.Effect<Info | undefined, AuthServiceError>
43+
readonly all: () => Effect.Effect<Record<string, Info>, AuthServiceError>
44+
readonly set: (key: string, info: Info) => Effect.Effect<void, AuthServiceError>
45+
readonly remove: (key: string) => Effect.Effect<void, AuthServiceError>
46+
}
47+
}
48+
49+
export class AuthService extends ServiceMap.Service<AuthService, AuthService.Service>()("@opencode/Auth") {
50+
static readonly layer = Layer.effect(
51+
AuthService,
52+
Effect.gen(function* () {
53+
const decode = Schema.decodeUnknownOption(Info)
54+
55+
const all = Effect.fn("AuthService.all")(() =>
56+
Effect.tryPromise({
57+
try: async () => {
58+
const data = await Filesystem.readJson<Record<string, unknown>>(file).catch(() => ({}))
59+
return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined))
60+
},
61+
catch: fail("Failed to read auth data"),
62+
}),
63+
)
64+
65+
const get = Effect.fn("AuthService.get")(function* (providerID: string) {
66+
return (yield* all())[providerID]
67+
})
68+
69+
const set = Effect.fn("AuthService.set")(function* (key: string, info: Info) {
70+
const norm = key.replace(/\/+$/, "")
71+
const data = yield* all()
72+
if (norm !== key) delete data[key]
73+
delete data[norm + "/"]
74+
yield* Effect.tryPromise({
75+
try: () => Filesystem.writeJson(file, { ...data, [norm]: info }, 0o600),
76+
catch: fail("Failed to write auth data"),
77+
})
78+
})
79+
80+
const remove = Effect.fn("AuthService.remove")(function* (key: string) {
81+
const norm = key.replace(/\/+$/, "")
82+
const data = yield* all()
83+
delete data[key]
84+
delete data[norm]
85+
yield* Effect.tryPromise({
86+
try: () => Filesystem.writeJson(file, data, 0o600),
87+
catch: fail("Failed to write auth data"),
88+
})
89+
})
90+
91+
return AuthService.of({
92+
get,
93+
all,
94+
set,
95+
remove,
96+
})
97+
}),
98+
)
99+
100+
static readonly defaultLayer = AuthService.layer
101+
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ManagedRuntime } from "effect"
1+
import { Layer, ManagedRuntime } from "effect"
22
import { AccountService } from "@/account/service"
3+
import { AuthService } from "@/auth/service"
34

4-
export const runtime = ManagedRuntime.make(AccountService.defaultLayer)
5+
export const runtime = ManagedRuntime.make(Layer.mergeAll(AccountService.defaultLayer, AuthService.defaultLayer))

0 commit comments

Comments
 (0)