8c94e9005f
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>
164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
import path from "path"
|
|
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
|
import { Global } from "@opencode-ai/core/global"
|
|
import { Effect, Layer, Context, Option, Schema } from "effect"
|
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
|
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
|
|
|
export const Tokens = Schema.Struct({
|
|
accessToken: Schema.mutableKey(Schema.String),
|
|
refreshToken: Schema.mutableKey(Schema.optional(Schema.String)),
|
|
expiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
|
|
scope: Schema.mutableKey(Schema.optional(Schema.String)),
|
|
})
|
|
export type Tokens = Schema.Schema.Type<typeof Tokens>
|
|
|
|
export const ClientInfo = Schema.Struct({
|
|
clientId: Schema.mutableKey(Schema.String),
|
|
clientSecret: Schema.mutableKey(Schema.optional(Schema.String)),
|
|
clientIdIssuedAt: Schema.mutableKey(Schema.optional(Schema.Number)),
|
|
clientSecretExpiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
|
|
})
|
|
export type ClientInfo = Schema.Schema.Type<typeof ClientInfo>
|
|
|
|
export const Entry = Schema.Struct({
|
|
tokens: Schema.mutableKey(Schema.optional(Tokens)),
|
|
clientInfo: Schema.mutableKey(Schema.optional(ClientInfo)),
|
|
codeVerifier: Schema.mutableKey(Schema.optional(Schema.String)),
|
|
oauthState: Schema.mutableKey(Schema.optional(Schema.String)),
|
|
serverUrl: Schema.mutableKey(Schema.optional(Schema.String)),
|
|
})
|
|
export type Entry = Schema.Schema.Type<typeof Entry>
|
|
|
|
const decodeAuthData = Schema.decodeUnknownOption(Schema.Record(Schema.String, Entry))
|
|
type AuthData = Record<string, Entry>
|
|
|
|
const filepath = path.join(Global.Path.data, "mcp-auth.json")
|
|
const lockKey = `mcp-auth:${filepath}`
|
|
|
|
export interface Interface {
|
|
readonly all: () => Effect.Effect<Record<string, Entry>>
|
|
readonly get: (mcpName: string) => Effect.Effect<Entry | undefined>
|
|
readonly getForUrl: (mcpName: string, serverUrl: string) => Effect.Effect<Entry | undefined>
|
|
readonly set: (mcpName: string, entry: Entry, serverUrl?: string) => Effect.Effect<void>
|
|
readonly remove: (mcpName: string) => Effect.Effect<void>
|
|
readonly updateTokens: (mcpName: string, tokens: Tokens, serverUrl?: string) => Effect.Effect<void>
|
|
readonly updateClientInfo: (mcpName: string, clientInfo: ClientInfo, serverUrl?: string) => Effect.Effect<void>
|
|
readonly updateCodeVerifier: (mcpName: string, codeVerifier: string) => Effect.Effect<void>
|
|
readonly clearCodeVerifier: (mcpName: string) => Effect.Effect<void>
|
|
readonly updateOAuthState: (mcpName: string, oauthState: string) => Effect.Effect<void>
|
|
readonly getOAuthState: (mcpName: string) => Effect.Effect<string | undefined>
|
|
readonly clearOAuthState: (mcpName: string) => Effect.Effect<void>
|
|
}
|
|
|
|
export class Service extends Context.Service<Service, Interface>()("@opencode/McpAuth") {}
|
|
|
|
export const use = serviceUse(Service)
|
|
|
|
const layer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const fs = yield* FSUtil.Service
|
|
const flock = yield* EffectFlock.Service
|
|
|
|
const read = Effect.fn("McpAuth.read")(function* () {
|
|
return yield* fs.readJson(filepath).pipe(
|
|
Effect.map((data): AuthData => Option.getOrElse(decodeAuthData(data), () => ({}) as AuthData) as AuthData),
|
|
Effect.catch(() => Effect.succeed({} as AuthData)),
|
|
)
|
|
})
|
|
|
|
const all = Effect.fn("McpAuth.all")(function* () {
|
|
return yield* read().pipe(flock.withLock(lockKey), Effect.orDie)
|
|
})
|
|
|
|
const mutate = Effect.fn("McpAuth.mutate")(function* (update: (data: AuthData) => AuthData | undefined) {
|
|
yield* Effect.gen(function* () {
|
|
const next = update(yield* read())
|
|
if (!next) return
|
|
yield* fs.writeJson(filepath, next, 0o600).pipe(Effect.orDie)
|
|
}).pipe(flock.withLock(lockKey), Effect.orDie)
|
|
})
|
|
|
|
const get = Effect.fn("McpAuth.get")(function* (mcpName: string) {
|
|
const data = yield* all()
|
|
return data[mcpName]
|
|
})
|
|
|
|
const getForUrl = Effect.fn("McpAuth.getForUrl")(function* (mcpName: string, serverUrl: string) {
|
|
const entry = yield* get(mcpName)
|
|
if (!entry) return undefined
|
|
if (!entry.serverUrl) return undefined
|
|
if (entry.serverUrl !== serverUrl) return undefined
|
|
return entry
|
|
})
|
|
|
|
const set = Effect.fn("McpAuth.set")(function* (mcpName: string, entry: Entry, serverUrl?: string) {
|
|
yield* mutate((data) => ({
|
|
...data,
|
|
[mcpName]: serverUrl ? { ...entry, serverUrl } : entry,
|
|
}))
|
|
})
|
|
|
|
const remove = Effect.fn("McpAuth.remove")(function* (mcpName: string) {
|
|
yield* mutate((data) => {
|
|
const next = { ...data }
|
|
delete next[mcpName]
|
|
return next
|
|
})
|
|
})
|
|
|
|
const updateField = <K extends keyof Entry>(field: K, spanName: string) =>
|
|
Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string, value: NonNullable<Entry[K]>, serverUrl?: string) {
|
|
yield* mutate((data) => {
|
|
const entry = data[mcpName] ?? {}
|
|
entry[field] = value
|
|
if (serverUrl) entry.serverUrl = serverUrl
|
|
return { ...data, [mcpName]: entry }
|
|
})
|
|
})
|
|
|
|
const clearField = (field: keyof Entry, spanName: string) =>
|
|
Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string) {
|
|
yield* mutate((data) => {
|
|
const entry = data[mcpName]
|
|
if (!entry) return undefined
|
|
delete entry[field]
|
|
return { ...data, [mcpName]: entry }
|
|
})
|
|
})
|
|
|
|
const updateTokens = updateField("tokens", "updateTokens")
|
|
const updateClientInfo = updateField("clientInfo", "updateClientInfo")
|
|
const updateCodeVerifier = updateField("codeVerifier", "updateCodeVerifier")
|
|
const updateOAuthState = updateField("oauthState", "updateOAuthState")
|
|
const clearCodeVerifier = clearField("codeVerifier", "clearCodeVerifier")
|
|
const clearOAuthState = clearField("oauthState", "clearOAuthState")
|
|
|
|
const getOAuthState = Effect.fn("McpAuth.getOAuthState")(function* (mcpName: string) {
|
|
const entry = yield* get(mcpName)
|
|
return entry?.oauthState
|
|
})
|
|
|
|
return Service.of({
|
|
all,
|
|
get,
|
|
getForUrl,
|
|
set,
|
|
remove,
|
|
updateTokens,
|
|
updateClientInfo,
|
|
updateCodeVerifier,
|
|
clearCodeVerifier,
|
|
updateOAuthState,
|
|
getOAuthState,
|
|
clearOAuthState,
|
|
})
|
|
}),
|
|
)
|
|
|
|
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node, EffectFlock.node] })
|
|
|
|
export * as McpAuth from "./auth"
|