refactor(auth): use Effect Schema internally

Model auth entries with Effect Schema inside AuthService and use Schema decoding when reading persisted auth data. Keep the Auth facade on Zod at the boundary so existing validators and callers stay stable during the migration.
This commit is contained in:
Kit Langton 2026-03-12 13:25:52 -04:00
commit 7f12976ea0
2 changed files with 60 additions and 49 deletions

View file

@ -1,27 +1,43 @@
import { Effect } from "effect"
import z from "zod"
import { runtime } from "@/effect/runtime"
import {
Api as ApiSchema,
AuthService,
type AuthServiceError,
Info as InfoSchema,
Oauth as OauthSchema,
WellKnown as WellKnownSchema,
type Info as AuthInfo,
} from "./service"
import * as S from "./service"
export { OAUTH_DUMMY_KEY } from "./service"
function runPromise<A>(f: (service: AuthService.Service) => Effect.Effect<A, AuthServiceError>) {
return runtime.runPromise(AuthService.use(f))
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 = OauthSchema
export const Api = ApiSchema
export const WellKnown = WellKnownSchema
export const Info = InfoSchema
export type Info = AuthInfo
export const Oauth = z
.object({
type: z.literal("oauth"),
refresh: z.string(),
access: z.string(),
expires: z.number(),
accountId: z.string().optional(),
enterpriseUrl: z.string().optional(),
})
.meta({ ref: "OAuth" })
export const Api = z
.object({
type: z.literal("api"),
key: z.string(),
})
.meta({ ref: "ApiAuth" })
export const WellKnown = z
.object({
type: z.literal("wellknown"),
key: z.string(),
token: z.string(),
})
.meta({ ref: "WellKnownAuth" })
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" })
export type Info = z.infer<typeof Info>
export async function get(providerID: string) {
return runPromise((service) => service.get(providerID))

View file

@ -1,39 +1,32 @@
import path from "path"
import { Effect, Layer, Schema, ServiceMap } from "effect"
import z from "zod"
import { Effect, Layer, Option, Schema, ServiceMap } from "effect"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
export const Oauth = z
.object({
type: z.literal("oauth"),
refresh: z.string(),
access: z.string(),
expires: z.number(),
accountId: z.string().optional(),
enterpriseUrl: z.string().optional(),
})
.meta({ ref: "OAuth" })
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 const Api = z
.object({
type: z.literal("api"),
key: z.string(),
})
.meta({ ref: "ApiAuth" })
export class Api extends Schema.Class<Api>("ApiAuth")({
type: Schema.Literal("api"),
key: Schema.String,
}) {}
export const WellKnown = z
.object({
type: z.literal("wellknown"),
key: z.string(),
token: z.string(),
})
.meta({ ref: "WellKnownAuth" })
export class WellKnown extends Schema.Class<WellKnown>("WellKnownAuth")({
type: Schema.Literal("wellknown"),
key: Schema.String,
token: Schema.String,
}) {}
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" })
export type Info = z.infer<typeof Info>
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,
@ -57,15 +50,17 @@ export class AuthService extends ServiceMap.Service<AuthService, AuthService.Ser
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 Object.entries(data).reduce(
(acc, [key, value]) => {
const parsed = Info.safeParse(value)
if (!parsed.success) return acc
acc[key] = parsed.data
const parsed = decode(value)
if (Option.isNone(parsed)) return acc
acc[key] = parsed.value
return acc
},
{} as Record<string, Info>,