| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import type { AuthOAuthResult, Hooks } from "@kirincode-ai/plugin"
- import { serviceUse } from "@kirincode-ai/core/effect/service-use"
- import { Auth } from "@/auth"
- import { InstanceState } from "@/effect/instance-state"
- import { optional } from "@kirincode-ai/core/schema"
- import { Plugin } from "../plugin"
- import { ProviderV2 } from "@kirincode-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>()("@kirincode/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"
|