diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts
index 80253a665e9..79e9e615d21 100644
--- a/packages/opencode/src/auth/index.ts
+++ b/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(f: (service: S.AuthService.Service) => Effect.Effect) {
+ 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
- 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> {
- const data = await Filesystem.readJson>(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,
- )
+ 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))
}
}
diff --git a/packages/opencode/src/auth/service.ts b/packages/opencode/src/auth/service.ts
new file mode 100644
index 00000000000..100a132b873
--- /dev/null
+++ b/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")({
+ 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("ApiAuth")({
+ type: Schema.Literal("api"),
+ key: Schema.String,
+}) {}
+
+export class WellKnown extends Schema.Class("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
+
+export class AuthServiceError extends Schema.TaggedErrorClass()("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
+ readonly all: () => Effect.Effect, AuthServiceError>
+ readonly set: (key: string, info: Info) => Effect.Effect
+ readonly remove: (key: string) => Effect.Effect
+ }
+}
+
+export class AuthService extends ServiceMap.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>(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
+}
diff --git a/packages/opencode/src/effect/runtime.ts b/packages/opencode/src/effect/runtime.ts
index 1868b38a015..23acff73379 100644
--- a/packages/opencode/src/effect/runtime.ts
+++ b/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))