refactor(instance): move scoped services to LayerMap (#17544)

This commit is contained in:
Kit Langton
2026-03-16 12:55:14 -04:00
committed by GitHub
parent 4cb29967f6
commit 469c3a4204
15 changed files with 154 additions and 497 deletions

View File

@@ -1,12 +1,9 @@
import { Effect, Layer, Record, ServiceMap, Struct } from "effect"
import { Instance } from "@/project/instance"
import { Plugin } from "../plugin"
import { filter, fromEntries, map, pipe } from "remeda"
import type { AuthOuathResult } from "@opencode-ai/plugin"
import { NamedError } from "@opencode-ai/util/error"
import * as Auth from "@/auth/service"
import { InstanceState } from "@/util/instance-state"
import { ProviderID } from "./schema"
import { Effect, Layer, Record, ServiceMap, Struct } from "effect"
import { filter, fromEntries, map, pipe } from "remeda"
import z from "zod"
export const Method = z
@@ -54,21 +51,13 @@ export type ProviderAuthError =
export namespace ProviderAuthService {
export interface Service {
/** Get available auth methods for each provider (e.g. OAuth, API key). */
readonly methods: () => Effect.Effect<Record<string, Method[]>>
/** Start an OAuth authorization flow for a provider. Returns the URL to redirect to. */
readonly authorize: (input: { providerID: ProviderID; method: number }) => Effect.Effect<Authorization | undefined>
/** Complete an OAuth flow after the user has authorized. Exchanges the code/callback for credentials. */
readonly callback: (input: {
providerID: ProviderID
method: number
code?: string
}) => Effect.Effect<void, ProviderAuthError>
/** Set an API key directly for a provider (no OAuth flow). */
readonly api: (input: { providerID: ProviderID; key: string }) => Effect.Effect<void, Auth.AuthServiceError>
}
}
@@ -79,32 +68,29 @@ export class ProviderAuthService extends ServiceMap.Service<ProviderAuthService,
ProviderAuthService,
Effect.gen(function* () {
const auth = yield* Auth.AuthService
const state = yield* InstanceState.make(() =>
Effect.promise(async () => {
const methods = pipe(
await Plugin.list(),
filter((x) => x.auth?.provider !== undefined),
map((x) => [x.auth!.provider, x.auth!] as const),
fromEntries(),
)
return { methods, pending: new Map<ProviderID, AuthOuathResult>() }
}),
)
const hooks = yield* Effect.promise(async () => {
const mod = await import("../plugin")
return pipe(
await mod.Plugin.list(),
filter((x) => x.auth?.provider !== undefined),
map((x) => [x.auth!.provider, x.auth!] as const),
fromEntries(),
)
})
const pending = new Map<ProviderID, AuthOuathResult>()
const methods = Effect.fn("ProviderAuthService.methods")(function* () {
const x = yield* InstanceState.get(state)
return Record.map(x.methods, (y) => y.methods.map((z): Method => Struct.pick(z, ["type", "label"])))
return Record.map(hooks, (item) => item.methods.map((method): Method => Struct.pick(method, ["type", "label"])))
})
const authorize = Effect.fn("ProviderAuthService.authorize")(function* (input: {
providerID: ProviderID
method: number
}) {
const s = yield* InstanceState.get(state)
const method = s.methods[input.providerID].methods[input.method]
const method = hooks[input.providerID].methods[input.method]
if (method.type !== "oauth") return
const result = yield* Effect.promise(() => method.authorize())
s.pending.set(input.providerID, result)
pending.set(input.providerID, result)
return {
url: result.url,
method: result.method,
@@ -117,17 +103,14 @@ export class ProviderAuthService extends ServiceMap.Service<ProviderAuthService,
method: number
code?: string
}) {
const s = yield* InstanceState.get(state)
const match = s.pending.get(input.providerID)
const match = pending.get(input.providerID)
if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
if (match.method === "code" && !input.code)
return yield* Effect.fail(new OauthCodeMissing({ providerID: input.providerID }))
const result = yield* Effect.promise(() =>
match.method === "code" ? match.callback(input.code!) : match.callback(),
)
if (!result || result.type !== "success") return yield* Effect.fail(new OauthCallbackFailed({}))
if ("key" in result) {
@@ -148,18 +131,10 @@ export class ProviderAuthService extends ServiceMap.Service<ProviderAuthService,
}
})
const api = Effect.fn("ProviderAuthService.api")(function* (input: { providerID: ProviderID; key: string }) {
yield* auth.set(input.providerID, {
type: "api",
key: input.key,
})
})
return ProviderAuthService.of({
methods,
authorize,
callback,
api,
})
}),
)

View File

@@ -1,25 +1,16 @@
import { Effect, ManagedRuntime } from "effect"
import z from "zod"
import { runPromiseInstance } from "@/effect/runtime"
import { fn } from "@/util/fn"
import * as S from "./auth-service"
import { ProviderID } from "./schema"
// Separate runtime: ProviderAuthService can't join the shared runtime because
// runtime.ts → auth-service.ts → provider/auth.ts creates a circular import.
// AuthService is stateless file I/O so the duplicate instance is harmless.
const rt = ManagedRuntime.make(S.ProviderAuthService.defaultLayer)
function runPromise<A>(f: (service: S.ProviderAuthService.Service) => Effect.Effect<A, S.ProviderAuthError>) {
return rt.runPromise(S.ProviderAuthService.use(f))
}
export namespace ProviderAuth {
export const Method = S.Method
export type Method = S.Method
export async function methods() {
return runPromise((service) => service.methods())
return runPromiseInstance(S.ProviderAuthService.use((service) => service.methods()))
}
export const Authorization = S.Authorization
@@ -30,7 +21,8 @@ export namespace ProviderAuth {
providerID: ProviderID.zod,
method: z.number(),
}),
async (input): Promise<Authorization | undefined> => runPromise((service) => service.authorize(input)),
async (input): Promise<Authorization | undefined> =>
runPromiseInstance(S.ProviderAuthService.use((service) => service.authorize(input))),
)
export const callback = fn(
@@ -39,15 +31,7 @@ export namespace ProviderAuth {
method: z.number(),
code: z.string().optional(),
}),
async (input) => runPromise((service) => service.callback(input)),
)
export const api = fn(
z.object({
providerID: ProviderID.zod,
key: z.string(),
}),
async (input) => runPromise((service) => service.api(input)),
async (input) => runPromiseInstance(S.ProviderAuthService.use((service) => service.callback(input))),
)
export import OauthMissing = S.OauthMissing