|
| 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 | +} |
0 commit comments