refactor(auth): remove async auth facade exports (#22306)

This commit is contained in:
Kit Langton 2026-04-13 12:31:43 -04:00 committed by GitHub
commit c22e34853d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 158 additions and 109 deletions

View file

@ -1,6 +1,5 @@
import path from "path" import path from "path"
import { Effect, Layer, Record, Result, Schema, Context } from "effect" import { Effect, Layer, Record, Result, Schema, Context } from "effect"
import { makeRuntime } from "@/effect/run-service"
import { zod } from "@/util/effect-zod" import { zod } from "@/util/effect-zod"
import { Global } from "../global" import { Global } from "../global"
import { AppFileSystem } from "../filesystem" import { AppFileSystem } from "../filesystem"
@ -89,22 +88,4 @@ export namespace Auth {
) )
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function get(providerID: string) {
return runPromise((service) => service.get(providerID))
}
export async function all(): Promise<Record<string, Info>> {
return runPromise((service) => service.all())
}
export async function set(key: string, info: Info) {
return runPromise((service) => service.set(key, info))
}
export async function remove(key: string) {
return runPromise((service) => service.remove(key))
}
} }

View file

@ -1,4 +1,5 @@
import { Auth } from "../../auth" import { Auth } from "../../auth"
import { AppRuntime } from "../../effect/app-runtime"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { UI } from "../ui" import { UI } from "../ui"
@ -13,9 +14,18 @@ import { Instance } from "../../project/instance"
import type { Hooks } from "@opencode-ai/plugin" import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "../../util/process" import { Process } from "../../util/process"
import { text } from "node:stream/consumers" import { text } from "node:stream/consumers"
import { Effect } from "effect"
type PluginAuth = NonNullable<Hooks["auth"]> type PluginAuth = NonNullable<Hooks["auth"]>
const put = (key: string, info: Auth.Info) =>
AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.set(key, info)
}),
)
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> { async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
let index = 0 let index = 0
if (methodName) { if (methodName) {
@ -93,7 +103,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
const saveProvider = result.provider ?? provider const saveProvider = result.provider ?? provider
if ("refresh" in result) { if ("refresh" in result) {
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
await Auth.set(saveProvider, { await put(saveProvider, {
type: "oauth", type: "oauth",
refresh, refresh,
access, access,
@ -102,7 +112,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
}) })
} }
if ("key" in result) { if ("key" in result) {
await Auth.set(saveProvider, { await put(saveProvider, {
type: "api", type: "api",
key: result.key, key: result.key,
}) })
@ -125,7 +135,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
const saveProvider = result.provider ?? provider const saveProvider = result.provider ?? provider
if ("refresh" in result) { if ("refresh" in result) {
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
await Auth.set(saveProvider, { await put(saveProvider, {
type: "oauth", type: "oauth",
refresh, refresh,
access, access,
@ -134,7 +144,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
}) })
} }
if ("key" in result) { if ("key" in result) {
await Auth.set(saveProvider, { await put(saveProvider, {
type: "api", type: "api",
key: result.key, key: result.key,
}) })
@ -161,7 +171,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
} }
if (result.type === "success") { if (result.type === "success") {
const saveProvider = result.provider ?? provider const saveProvider = result.provider ?? provider
await Auth.set(saveProvider, { await put(saveProvider, {
type: "api", type: "api",
key: result.key ?? key, key: result.key ?? key,
}) })
@ -221,7 +231,12 @@ export const ProvidersListCommand = cmd({
const homedir = os.homedir() const homedir = os.homedir()
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`) prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
const results = Object.entries(await Auth.all()) const results = await AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
return Object.entries(yield* auth.all())
}),
)
const database = await ModelsDev.get() const database = await ModelsDev.get()
for (const [providerID, result] of results) { for (const [providerID, result] of results) {
@ -300,7 +315,7 @@ export const ProvidersLoginCommand = cmd({
prompts.outro("Done") prompts.outro("Done")
return return
} }
await Auth.set(url, { await put(url, {
type: "wellknown", type: "wellknown",
key: wellknown.auth.env, key: wellknown.auth.env,
token: token.trim(), token: token.trim(),
@ -447,7 +462,7 @@ export const ProvidersLoginCommand = cmd({
validate: (x) => (x && x.length > 0 ? undefined : "Required"), validate: (x) => (x && x.length > 0 ? undefined : "Required"),
}) })
if (prompts.isCancel(key)) throw new UI.CancelledError() if (prompts.isCancel(key)) throw new UI.CancelledError()
await Auth.set(provider, { await put(provider, {
type: "api", type: "api",
key, key,
}) })
@ -463,22 +478,33 @@ export const ProvidersLogoutCommand = cmd({
describe: "log out from a configured provider", describe: "log out from a configured provider",
async handler(_args) { async handler(_args) {
UI.empty() UI.empty()
const credentials = await Auth.all().then((x) => Object.entries(x)) const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
return Object.entries(yield* auth.all())
}),
)
prompts.intro("Remove credential") prompts.intro("Remove credential")
if (credentials.length === 0) { if (credentials.length === 0) {
prompts.log.error("No credentials found") prompts.log.error("No credentials found")
return return
} }
const database = await ModelsDev.get() const database = await ModelsDev.get()
const providerID = await prompts.select({ const selected = await prompts.select({
message: "Select provider", message: "Select provider",
options: credentials.map(([key, value]) => ({ options: credentials.map(([key, value]) => ({
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")", label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
value: key, value: key,
})), })),
}) })
if (prompts.isCancel(providerID)) throw new UI.CancelledError() if (prompts.isCancel(selected)) throw new UI.CancelledError()
await Auth.remove(providerID) const providerID = selected as string
await AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove(providerID)
}),
)
prompts.outro("Logout successful") prompts.outro("Logout successful")
}, },
}) })

View file

@ -1,5 +1,7 @@
import { Auth } from "@/auth" import { Auth } from "@/auth"
import { AppRuntime } from "@/effect/app-runtime"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { Effect } from "effect"
import { ProviderID } from "@/provider/schema" import { ProviderID } from "@/provider/schema"
import { Hono } from "hono" import { Hono } from "hono"
import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi" import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi"
@ -39,7 +41,12 @@ export function ControlPlaneRoutes(): Hono {
async (c) => { async (c) => {
const providerID = c.req.valid("param").providerID const providerID = c.req.valid("param").providerID
const info = c.req.valid("json") const info = c.req.valid("json")
await Auth.set(providerID, info) await AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.set(providerID, info)
}),
)
return c.json(true) return c.json(true)
}, },
) )
@ -69,7 +76,12 @@ export function ControlPlaneRoutes(): Hono {
), ),
async (c) => { async (c) => {
const providerID = c.req.valid("param").providerID const providerID = c.req.valid("param").providerID
await Auth.remove(providerID) await AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove(providerID)
}),
)
return c.json(true) return c.json(true)
}, },
) )

View file

@ -94,14 +94,24 @@ export namespace LLM {
modelID: input.model.id, modelID: input.model.id,
providerID: input.model.providerID, providerID: input.model.providerID,
}) })
const [language, cfg, provider, auth] = await Promise.all([ const [language, cfg, provider, info] = await Effect.runPromise(
Provider.getLanguage(input.model), Effect.gen(function* () {
Config.get(), const auth = yield* Auth.Service
Provider.getProvider(input.model.providerID), const cfg = yield* Config.Service
Auth.get(input.model.providerID), const provider = yield* Provider.Service
]) return yield* Effect.all(
[
provider.getLanguage(input.model),
cfg.get(),
provider.getProvider(input.model.providerID),
auth.get(input.model.providerID),
],
{ concurrency: "unbounded" },
)
}).pipe(Effect.provide(Layer.mergeAll(Auth.defaultLayer, Config.defaultLayer, Provider.defaultLayer))),
)
// TODO: move this to a proper hook // TODO: move this to a proper hook
const isOpenaiOauth = provider.id === "openai" && auth?.type === "oauth" const isOpenaiOauth = provider.id === "openai" && info?.type === "oauth"
const system: string[] = [] const system: string[] = []
system.push( system.push(
@ -200,7 +210,7 @@ export namespace LLM {
}, },
) )
const tools = await resolveTools(input) const tools = resolveTools(input)
// LiteLLM and some Anthropic proxies require the tools parameter to be present // LiteLLM and some Anthropic proxies require the tools parameter to be present
// when message history contains tool calls, even if no tools are being used. // when message history contains tool calls, even if no tools are being used.

View file

@ -1,58 +1,86 @@
import { test, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Auth } from "../../src/auth" import { Auth } from "../../src/auth"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
test("set normalizes trailing slashes in keys", async () => { const node = CrossSpawnSpawner.defaultLayer
await Auth.set("https://example.com/", {
type: "wellknown",
key: "TOKEN",
token: "abc",
})
const data = await Auth.all()
expect(data["https://example.com"]).toBeDefined()
expect(data["https://example.com/"]).toBeUndefined()
})
test("set cleans up pre-existing trailing-slash entry", async () => { const it = testEffect(Layer.mergeAll(Auth.defaultLayer, node))
// Simulate a pre-fix entry with trailing slash
await Auth.set("https://example.com/", {
type: "wellknown",
key: "TOKEN",
token: "old",
})
// Re-login with normalized key (as the CLI does post-fix)
await Auth.set("https://example.com", {
type: "wellknown",
key: "TOKEN",
token: "new",
})
const data = await Auth.all()
const keys = Object.keys(data).filter((k) => k.includes("example.com"))
expect(keys).toEqual(["https://example.com"])
const entry = data["https://example.com"]!
expect(entry.type).toBe("wellknown")
if (entry.type === "wellknown") expect(entry.token).toBe("new")
})
test("remove deletes both trailing-slash and normalized keys", async () => { describe("Auth", () => {
await Auth.set("https://example.com", { it.live("set normalizes trailing slashes in keys", () =>
type: "wellknown", provideTmpdirInstance(() =>
key: "TOKEN", Effect.gen(function* () {
token: "abc", const auth = yield* Auth.Service
}) yield* auth.set("https://example.com/", {
await Auth.remove("https://example.com/") type: "wellknown",
const data = await Auth.all() key: "TOKEN",
expect(data["https://example.com"]).toBeUndefined() token: "abc",
expect(data["https://example.com/"]).toBeUndefined() })
}) const data = yield* auth.all()
expect(data["https://example.com"]).toBeDefined()
expect(data["https://example.com/"]).toBeUndefined()
}),
),
)
test("set and remove are no-ops on keys without trailing slashes", async () => { it.live("set cleans up pre-existing trailing-slash entry", () =>
await Auth.set("anthropic", { provideTmpdirInstance(() =>
type: "api", Effect.gen(function* () {
key: "sk-test", const auth = yield* Auth.Service
}) yield* auth.set("https://example.com/", {
const data = await Auth.all() type: "wellknown",
expect(data["anthropic"]).toBeDefined() key: "TOKEN",
await Auth.remove("anthropic") token: "old",
const after = await Auth.all() })
expect(after["anthropic"]).toBeUndefined() yield* auth.set("https://example.com", {
type: "wellknown",
key: "TOKEN",
token: "new",
})
const data = yield* auth.all()
const keys = Object.keys(data).filter((key) => key.includes("example.com"))
expect(keys).toEqual(["https://example.com"])
const entry = data["https://example.com"]!
expect(entry.type).toBe("wellknown")
if (entry.type === "wellknown") expect(entry.token).toBe("new")
}),
),
)
it.live("remove deletes both trailing-slash and normalized keys", () =>
provideTmpdirInstance(() =>
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.set("https://example.com", {
type: "wellknown",
key: "TOKEN",
token: "abc",
})
yield* auth.remove("https://example.com/")
const data = yield* auth.all()
expect(data["https://example.com"]).toBeUndefined()
expect(data["https://example.com/"]).toBeUndefined()
}),
),
)
it.live("set and remove are no-ops on keys without trailing slashes", () =>
provideTmpdirInstance(() =>
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.set("anthropic", {
type: "api",
key: "sk-test",
})
const data = yield* auth.all()
expect(data["anthropic"]).toBeDefined()
yield* auth.remove("anthropic")
const after = yield* auth.all()
expect(after["anthropic"]).toBeUndefined()
}),
),
)
}) })

View file

@ -1,10 +1,10 @@
import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Deferred, Effect, Layer, Stream } from "effect" import { Deferred, Effect, Layer, Stream } from "effect"
import z from "zod" import z from "zod"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { BusEvent } from "../../src/bus/bus-event" import { BusEvent } from "../../src/bus/bus-event"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
@ -13,9 +13,7 @@ const TestEvent = {
Pong: BusEvent.define("test.effect.pong", z.object({ message: z.string() })), Pong: BusEvent.define("test.effect.pong", z.object({ message: z.string() })),
} }
const node = NodeChildProcessSpawner.layer.pipe( const node = CrossSpawnSpawner.defaultLayer
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
)
const live = Layer.mergeAll(Bus.layer, node) const live = Layer.mergeAll(Bus.layer, node)

View file

@ -1,15 +1,13 @@
import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { Skill } from "../../src/skill" import { Skill } from "../../src/skill"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture" import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import path from "path" import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
const node = NodeChildProcessSpawner.layer.pipe( const node = CrossSpawnSpawner.defaultLayer
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
)
const it = testEffect(Layer.mergeAll(Skill.defaultLayer, node)) const it = testEffect(Layer.mergeAll(Skill.defaultLayer, node))

View file

@ -1,16 +1,14 @@
import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
import { afterEach, describe, expect } from "bun:test" import { afterEach, describe, expect } from "bun:test"
import path from "path" import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { ToolRegistry } from "../../src/tool/registry" import { ToolRegistry } from "../../src/tool/registry"
import { provideTmpdirInstance } from "../fixture/fixture" import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const node = NodeChildProcessSpawner.layer.pipe( const node = CrossSpawnSpawner.defaultLayer
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
)
const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node)) const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))

View file

@ -1,7 +1,7 @@
import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
import { Effect, Layer, ManagedRuntime } from "effect" import { Effect, Layer, ManagedRuntime } from "effect"
import { Agent } from "../../src/agent/agent" import { Agent } from "../../src/agent/agent"
import { Skill } from "../../src/skill" import { Skill } from "../../src/skill"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Ripgrep } from "../../src/file/ripgrep" import { Ripgrep } from "../../src/file/ripgrep"
import { Truncate } from "../../src/tool/truncate" import { Truncate } from "../../src/tool/truncate"
import { afterEach, describe, expect, test } from "bun:test" import { afterEach, describe, expect, test } from "bun:test"
@ -30,9 +30,7 @@ afterEach(async () => {
await Instance.disposeAll() await Instance.disposeAll()
}) })
const node = NodeChildProcessSpawner.layer.pipe( const node = CrossSpawnSpawner.defaultLayer
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
)
const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node)) const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))