|
1 | | -export * as Auth from "./auth" |
2 | | -export { OAUTH_DUMMY_KEY } from "./auth" |
| 1 | +import path from "path" |
| 2 | +import { Effect, Layer, Record, Result, Schema, Context } from "effect" |
| 3 | +import { zod } from "@/util/effect-zod" |
| 4 | +import { Global } from "../global" |
| 5 | +import { AppFileSystem } from "@opencode-ai/shared/filesystem" |
| 6 | + |
| 7 | +export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" |
| 8 | + |
| 9 | +const file = path.join(Global.Path.data, "auth.json") |
| 10 | + |
| 11 | +const fail = (message: string) => (cause: unknown) => new AuthError({ message, cause }) |
| 12 | + |
| 13 | +export class Oauth extends Schema.Class<Oauth>("OAuth")({ |
| 14 | + type: Schema.Literal("oauth"), |
| 15 | + refresh: Schema.String, |
| 16 | + access: Schema.String, |
| 17 | + expires: Schema.Number, |
| 18 | + accountId: Schema.optional(Schema.String), |
| 19 | + enterpriseUrl: Schema.optional(Schema.String), |
| 20 | +}) {} |
| 21 | + |
| 22 | +export class Api extends Schema.Class<Api>("ApiAuth")({ |
| 23 | + type: Schema.Literal("api"), |
| 24 | + key: Schema.String, |
| 25 | + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), |
| 26 | +}) {} |
| 27 | + |
| 28 | +export class WellKnown extends Schema.Class<WellKnown>("WellKnownAuth")({ |
| 29 | + type: Schema.Literal("wellknown"), |
| 30 | + key: Schema.String, |
| 31 | + token: Schema.String, |
| 32 | +}) {} |
| 33 | + |
| 34 | +const _Info = Schema.Union([Oauth, Api, WellKnown]).annotate({ discriminator: "type", identifier: "Auth" }) |
| 35 | +export const Info = Object.assign(_Info, { zod: zod(_Info) }) |
| 36 | +export type Info = Schema.Schema.Type<typeof _Info> |
| 37 | + |
| 38 | +export class AuthError extends Schema.TaggedErrorClass<AuthError>()("AuthError", { |
| 39 | + message: Schema.String, |
| 40 | + cause: Schema.optional(Schema.Defect), |
| 41 | +}) {} |
| 42 | + |
| 43 | +export interface Interface { |
| 44 | + readonly get: (providerID: string) => Effect.Effect<Info | undefined, AuthError> |
| 45 | + readonly all: () => Effect.Effect<Record<string, Info>, AuthError> |
| 46 | + readonly set: (key: string, info: Info) => Effect.Effect<void, AuthError> |
| 47 | + readonly remove: (key: string) => Effect.Effect<void, AuthError> |
| 48 | +} |
| 49 | + |
| 50 | +export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {} |
| 51 | + |
| 52 | +export const layer = Layer.effect( |
| 53 | + Service, |
| 54 | + Effect.gen(function* () { |
| 55 | + const fsys = yield* AppFileSystem.Service |
| 56 | + const decode = Schema.decodeUnknownOption(Info) |
| 57 | + |
| 58 | + const all = Effect.fn("Auth.all")(function* () { |
| 59 | + if (process.env.OPENCODE_AUTH_CONTENT) { |
| 60 | + try { |
| 61 | + return JSON.parse(process.env.OPENCODE_AUTH_CONTENT) |
| 62 | + } catch (err) {} |
| 63 | + } |
| 64 | + |
| 65 | + const data = (yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => ({})))) as Record<string, unknown> |
| 66 | + return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined)) |
| 67 | + }) |
| 68 | + |
| 69 | + const get = Effect.fn("Auth.get")(function* (providerID: string) { |
| 70 | + return (yield* all())[providerID] |
| 71 | + }) |
| 72 | + |
| 73 | + const set = Effect.fn("Auth.set")(function* (key: string, info: Info) { |
| 74 | + const norm = key.replace(/\/+$/, "") |
| 75 | + const data = yield* all() |
| 76 | + if (norm !== key) delete data[key] |
| 77 | + delete data[norm + "/"] |
| 78 | + yield* fsys |
| 79 | + .writeJson(file, { ...data, [norm]: info }, 0o600) |
| 80 | + .pipe(Effect.mapError(fail("Failed to write auth data"))) |
| 81 | + }) |
| 82 | + |
| 83 | + const remove = Effect.fn("Auth.remove")(function* (key: string) { |
| 84 | + const norm = key.replace(/\/+$/, "") |
| 85 | + const data = yield* all() |
| 86 | + delete data[key] |
| 87 | + delete data[norm] |
| 88 | + yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data"))) |
| 89 | + }) |
| 90 | + |
| 91 | + return Service.of({ get, all, set, remove }) |
| 92 | + }), |
| 93 | +) |
| 94 | + |
| 95 | +export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) |
| 96 | + |
| 97 | +export * as Auth from "." |
0 commit comments