import path from "path" import { Effect, Layer, Record, Result, Schema, Context } from "effect" import { zod } from "@/util/effect-zod" import { Global } from "../global" import { AppFileSystem } from "@opencode-ai/shared/filesystem" export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" const file = path.join(Global.Path.data, "auth.json") const fail = (message: string) => (cause: unknown) => new AuthError({ message, cause }) 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, metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), }) {} export class WellKnown extends Schema.Class("WellKnownAuth")({ type: Schema.Literal("wellknown"), key: Schema.String, token: Schema.String, }) {} const _Info = Schema.Union([Oauth, Api, WellKnown]).annotate({ discriminator: "type", identifier: "Auth" }) export const Info = Object.assign(_Info, { zod: zod(_Info) }) export type Info = Schema.Schema.Type export class AuthError extends Schema.TaggedErrorClass()("AuthError", { message: Schema.String, cause: Schema.optional(Schema.Defect), }) {} export interface Interface { readonly get: (providerID: string) => Effect.Effect readonly all: () => Effect.Effect, AuthError> readonly set: (key: string, info: Info) => Effect.Effect readonly remove: (key: string) => Effect.Effect } export class Service extends Context.Service()("@opencode/Auth") {} export const layer = Layer.effect( Service, Effect.gen(function* () { const fsys = yield* AppFileSystem.Service const decode = Schema.decodeUnknownOption(Info) const all = Effect.fn("Auth.all")(function* () { const data = (yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => ({})))) as Record return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined)) }) const get = Effect.fn("Auth.get")(function* (providerID: string) { return (yield* all())[providerID] }) const set = Effect.fn("Auth.set")(function* (key: string, info: Info) { const norm = key.replace(/\/+$/, "") const data = yield* all() if (norm !== key) delete data[key] delete data[norm + "/"] yield* fsys .writeJson(file, { ...data, [norm]: info }, 0o600) .pipe(Effect.mapError(fail("Failed to write auth data"))) }) const remove = Effect.fn("Auth.remove")(function* (key: string) { const norm = key.replace(/\/+$/, "") const data = yield* all() delete data[key] delete data[norm] yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data"))) }) return Service.of({ get, all, set, remove }) }), ) export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))