Files
anomalyco_opencode/packages/opencode/src/provider/auth.ts
T
James Long 8c94e9005f chore: merge dev into v2 (#34788)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com>
Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Ben Guthrie <benjee.012@gmail.com>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
Co-authored-by: Max Anderson <max.a.anderson95@gmail.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: runvip <164729189+runvip@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
2026-07-01 17:12:00 -04:00

230 lines
7.7 KiB
TypeScript

import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Auth } from "@/auth"
import { InstanceState } from "@/effect/instance-state"
import { optional } from "@opencode-ai/core/schema"
import { Plugin } from "../plugin"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
const When = Schema.Struct({
key: Schema.String,
op: Schema.Literals(["eq", "neq"]),
value: Schema.String,
})
const TextPrompt = Schema.Struct({
type: Schema.Literal("text"),
key: Schema.String,
message: Schema.String,
placeholder: optional(Schema.String),
when: optional(When),
})
const SelectOption = Schema.Struct({
label: Schema.String,
value: Schema.String,
hint: optional(Schema.String),
})
const SelectPrompt = Schema.Struct({
type: Schema.Literal("select"),
key: Schema.String,
message: Schema.String,
options: Schema.Array(SelectOption),
when: optional(When),
})
const Prompt = Schema.Union([TextPrompt, SelectPrompt])
export class Method extends Schema.Class<Method>("ProviderAuthMethod")({
type: Schema.Literals(["oauth", "api"]),
label: Schema.String,
prompts: optional(Schema.Array(Prompt)),
}) {}
export const Methods = Schema.Record(Schema.String, Schema.Array(Method))
export type Methods = typeof Methods.Type
export class Authorization extends Schema.Class<Authorization>("ProviderAuthAuthorization")({
url: Schema.String,
method: Schema.Literals(["auto", "code"]),
instructions: Schema.String,
}) {}
export const AuthorizeInput = Schema.Struct({
method: Schema.Finite.annotate({ description: "Auth method index" }),
inputs: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({ description: "Prompt inputs" }),
})
export type AuthorizeInput = Schema.Schema.Type<typeof AuthorizeInput>
export const CallbackInput = Schema.Struct({
method: Schema.Finite.annotate({ description: "Auth method index" }),
code: Schema.optional(Schema.String).annotate({ description: "OAuth authorization code" }),
})
export type CallbackInput = Schema.Schema.Type<typeof CallbackInput>
export class OauthMissing extends Schema.TaggedErrorClass<OauthMissing>()("ProviderAuthOauthMissing", {
providerID: ProviderV2.ID,
}) {}
export class OauthCodeMissing extends Schema.TaggedErrorClass<OauthCodeMissing>()("ProviderAuthOauthCodeMissing", {
providerID: ProviderV2.ID,
}) {}
export class OauthCallbackFailed extends Schema.TaggedErrorClass<OauthCallbackFailed>()(
"ProviderAuthOauthCallbackFailed",
{},
) {}
export class ValidationFailed extends Schema.TaggedErrorClass<ValidationFailed>()("ProviderAuthValidationFailed", {
field: Schema.String,
message: Schema.String,
}) {}
export type Error = Auth.AuthError | OauthMissing | OauthCodeMissing | OauthCallbackFailed | ValidationFailed
type Hook = NonNullable<Hooks["auth"]>
export interface Interface {
readonly methods: () => Effect.Effect<Methods>
readonly authorize: (
input: {
providerID: ProviderV2.ID
} & AuthorizeInput,
) => Effect.Effect<Authorization | undefined, Error>
readonly callback: (input: { providerID: ProviderV2.ID } & CallbackInput) => Effect.Effect<void, Error>
}
interface State {
hooks: Record<ProviderV2.ID, Hook>
pending: Map<ProviderV2.ID, AuthOAuthResult>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
export const use = serviceUse(Service)
const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
const plugin = yield* Plugin.Service
const state = yield* InstanceState.make<State>(
Effect.fn("ProviderAuth.state")(function* () {
const plugins = yield* plugin.list()
return {
hooks: Record.fromEntries(
Arr.filterMap(plugins, (x) =>
x.auth?.provider !== undefined
? Result.succeed([ProviderV2.ID.make(x.auth.provider), x.auth] as const)
: Result.failVoid,
),
),
pending: new Map<ProviderV2.ID, AuthOAuthResult>(),
}
}),
)
const decode = Schema.decodeUnknownSync(Methods)
const methods = Effect.fn("ProviderAuth.methods")(function* () {
const hooks = (yield* InstanceState.get(state)).hooks
return decode(
Record.map(hooks, (item) =>
item.methods.map((method) => ({
type: method.type,
label: method.label,
...(method.prompts && {
prompts: method.prompts.map((prompt) => {
if (prompt.type === "select") {
return {
type: "select" as const,
key: prompt.key,
message: prompt.message,
options: prompt.options,
...(prompt.when && { when: prompt.when }),
}
}
return {
type: "text" as const,
key: prompt.key,
message: prompt.message,
...(prompt.placeholder && { placeholder: prompt.placeholder }),
...(prompt.when && { when: prompt.when }),
}
}),
}),
})),
),
)
})
const authorize = Effect.fn("ProviderAuth.authorize")(function* (
input: { providerID: ProviderV2.ID } & AuthorizeInput,
) {
const { hooks, pending } = yield* InstanceState.get(state)
const method = hooks[input.providerID].methods[input.method]
if (method.type !== "oauth") return
if (method.prompts && input.inputs) {
for (const prompt of method.prompts) {
if (prompt.type === "text" && prompt.validate && input.inputs[prompt.key] !== undefined) {
const error = prompt.validate(input.inputs[prompt.key])
if (error) return yield* new ValidationFailed({ field: prompt.key, message: error })
}
}
}
const result = yield* Effect.promise(() => method.authorize(input.inputs))
pending.set(input.providerID, result)
return {
url: result.url,
method: result.method,
instructions: result.instructions,
}
})
const callback = Effect.fn("ProviderAuth.callback")(function* (
input: { providerID: ProviderV2.ID } & CallbackInput,
) {
const pending = (yield* InstanceState.get(state)).pending
const match = pending.get(input.providerID)
if (!match) return yield* new OauthMissing({ providerID: input.providerID })
if (match.method === "code" && !input.code) {
return yield* 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* new OauthCallbackFailed({})
if ("key" in result) {
yield* auth.set(input.providerID, {
type: "api",
key: result.key,
...(result.metadata ? { metadata: result.metadata } : {}),
})
}
if ("refresh" in result) {
const { type: _, provider: __, refresh, access, expires, ...extra } = result
yield* auth.set(input.providerID, {
type: "oauth",
access,
refresh,
expires,
...extra,
})
}
})
return Service.of({ methods, authorize, callback })
}),
)
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Auth.node, Plugin.node] })
export * as ProviderAuth from "./auth"