Преглед изворни кода

refactor(auth): effectify AuthService (#17212)

Kit Langton пре 5 месеци
родитељ
комит
0a281c7390

+ 12 - 28
packages/opencode/src/auth/index.ts

@@ -1,9 +1,13 @@
-import path from "path"
-import { Global } from "../global"
+import { Effect } from "effect"
 import z from "zod"
-import { Filesystem } from "../util/filesystem"
+import { runtime } from "@/effect/runtime"
+import * as S from "./service"
 
-export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
+export { OAUTH_DUMMY_KEY } from "./service"
+
+function runPromise<A>(f: (service: S.AuthService.Service) => Effect.Effect<A, S.AuthServiceError>) {
+  return runtime.runPromise(S.AuthService.use(f))
+}
 
 export namespace Auth {
   export const Oauth = z
@@ -35,39 +39,19 @@ export namespace Auth {
   export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" })
   export type Info = z.infer<typeof Info>
 
-  const filepath = path.join(Global.Path.data, "auth.json")
-
   export async function get(providerID: string) {
-    const auth = await all()
-    return auth[providerID]
+    return runPromise((service) => service.get(providerID))
   }
 
   export async function all(): Promise<Record<string, Info>> {
-    const data = await Filesystem.readJson<Record<string, unknown>>(filepath).catch(() => ({}))
-    return Object.entries(data).reduce(
-      (acc, [key, value]) => {
-        const parsed = Info.safeParse(value)
-        if (!parsed.success) return acc
-        acc[key] = parsed.data
-        return acc
-      },
-      {} as Record<string, Info>,
-    )
+    return runPromise((service) => service.all())
   }
 
   export async function set(key: string, info: Info) {
-    const normalized = key.replace(/\/+$/, "")
-    const data = await all()
-    if (normalized !== key) delete data[key]
-    delete data[normalized + "/"]
-    await Filesystem.writeJson(filepath, { ...data, [normalized]: info }, 0o600)
+    return runPromise((service) => service.set(key, info))
   }
 
   export async function remove(key: string) {
-    const normalized = key.replace(/\/+$/, "")
-    const data = await all()
-    delete data[key]
-    delete data[normalized]
-    await Filesystem.writeJson(filepath, data, 0o600)
+    return runPromise((service) => service.remove(key))
   }
 }

+ 101 - 0
packages/opencode/src/auth/service.ts

@@ -0,0 +1,101 @@
+import path from "path"
+import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
+import { Global } from "../global"
+import { Filesystem } from "../util/filesystem"
+
+export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
+
+export class Oauth extends Schema.Class<Oauth>("OAuth")({
+  type: Schema.Literal("oauth"),
+  refresh: Schema.String,
+  access: Schema.String,
+  expires: Schema.Number,
+  accountId: Schema.optional(Schema.String),
+  enterpriseUrl: Schema.optional(Schema.String),
+}) {}
+
+export class Api extends Schema.Class<Api>("ApiAuth")({
+  type: Schema.Literal("api"),
+  key: Schema.String,
+}) {}
+
+export class WellKnown extends Schema.Class<WellKnown>("WellKnownAuth")({
+  type: Schema.Literal("wellknown"),
+  key: Schema.String,
+  token: Schema.String,
+}) {}
+
+export const Info = Schema.Union([Oauth, Api, WellKnown])
+export type Info = Schema.Schema.Type<typeof Info>
+
+export class AuthServiceError extends Schema.TaggedErrorClass<AuthServiceError>()("AuthServiceError", {
+  message: Schema.String,
+  cause: Schema.optional(Schema.Defect),
+}) {}
+
+const file = path.join(Global.Path.data, "auth.json")
+
+const fail = (message: string) => (cause: unknown) => new AuthServiceError({ message, cause })
+
+export namespace AuthService {
+  export interface Service {
+    readonly get: (providerID: string) => Effect.Effect<Info | undefined, AuthServiceError>
+    readonly all: () => Effect.Effect<Record<string, Info>, AuthServiceError>
+    readonly set: (key: string, info: Info) => Effect.Effect<void, AuthServiceError>
+    readonly remove: (key: string) => Effect.Effect<void, AuthServiceError>
+  }
+}
+
+export class AuthService extends ServiceMap.Service<AuthService, AuthService.Service>()("@opencode/Auth") {
+  static readonly layer = Layer.effect(
+    AuthService,
+    Effect.gen(function* () {
+      const decode = Schema.decodeUnknownOption(Info)
+
+      const all = Effect.fn("AuthService.all")(() =>
+        Effect.tryPromise({
+          try: async () => {
+            const data = await Filesystem.readJson<Record<string, unknown>>(file).catch(() => ({}))
+            return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined))
+          },
+          catch: fail("Failed to read auth data"),
+        }),
+      )
+
+      const get = Effect.fn("AuthService.get")(function* (providerID: string) {
+        return (yield* all())[providerID]
+      })
+
+      const set = Effect.fn("AuthService.set")(function* (key: string, info: Info) {
+        const norm = key.replace(/\/+$/, "")
+        const data = yield* all()
+        if (norm !== key) delete data[key]
+        delete data[norm + "/"]
+        yield* Effect.tryPromise({
+          try: () => Filesystem.writeJson(file, { ...data, [norm]: info }, 0o600),
+          catch: fail("Failed to write auth data"),
+        })
+      })
+
+      const remove = Effect.fn("AuthService.remove")(function* (key: string) {
+        const norm = key.replace(/\/+$/, "")
+        const data = yield* all()
+        delete data[key]
+        delete data[norm]
+        yield* Effect.tryPromise({
+          try: () => Filesystem.writeJson(file, data, 0o600),
+          catch: fail("Failed to write auth data"),
+        })
+      })
+
+      return AuthService.of({
+        get,
+        all,
+        set,
+        remove,
+      })
+    }),
+  )
+
+  static readonly defaultLayer = AuthService.layer
+}

+ 3 - 2
packages/opencode/src/effect/runtime.ts

@@ -1,4 +1,5 @@
-import { ManagedRuntime } from "effect"
+import { Layer, ManagedRuntime } from "effect"
 import { AccountService } from "@/account/service"
+import { AuthService } from "@/auth/service"
 
-export const runtime = ManagedRuntime.make(AccountService.defaultLayer)
+export const runtime = ManagedRuntime.make(Layer.mergeAll(AccountService.defaultLayer, AuthService.defaultLayer))