7ba41d388a
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
156 lines
3.5 KiB
TypeScript
156 lines
3.5 KiB
TypeScript
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"
|
|
import { dirname, join } from "node:path"
|
|
import { getOpenCodeConfigDir } from "../../shared"
|
|
|
|
export interface OAuthTokenData {
|
|
accessToken: string
|
|
refreshToken?: string
|
|
expiresAt?: number
|
|
clientInfo?: {
|
|
clientId: string
|
|
clientSecret?: string
|
|
}
|
|
}
|
|
|
|
type TokenStore = Record<string, OAuthTokenData>
|
|
|
|
const STORAGE_FILE_NAME = "mcp-oauth.json"
|
|
|
|
export function getMcpOauthStoragePath(): string {
|
|
return join(getOpenCodeConfigDir({ binary: "opencode" }), STORAGE_FILE_NAME)
|
|
}
|
|
|
|
function normalizeHost(serverHost: string): string {
|
|
let host = serverHost.trim()
|
|
if (!host) return host
|
|
|
|
if (host.includes("://")) {
|
|
try {
|
|
host = new URL(host).hostname
|
|
} catch {
|
|
host = host.split("/")[0]
|
|
}
|
|
} else {
|
|
host = host.split("/")[0]
|
|
}
|
|
|
|
if (host.startsWith("[")) {
|
|
const closing = host.indexOf("]")
|
|
if (closing !== -1) {
|
|
host = host.slice(0, closing + 1)
|
|
}
|
|
return host
|
|
}
|
|
|
|
if (host.includes(":")) {
|
|
host = host.split(":")[0]
|
|
}
|
|
|
|
return host
|
|
}
|
|
|
|
function normalizeResource(resource: string): string {
|
|
return resource.replace(/^\/+/, "")
|
|
}
|
|
|
|
function buildKey(serverHost: string, resource: string): string {
|
|
const host = normalizeHost(serverHost)
|
|
const normalizedResource = normalizeResource(resource)
|
|
return `${host}/${normalizedResource}`
|
|
}
|
|
|
|
function readStore(): TokenStore | null {
|
|
const filePath = getMcpOauthStoragePath()
|
|
if (!existsSync(filePath)) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const content = readFileSync(filePath, "utf-8")
|
|
return JSON.parse(content) as TokenStore
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function writeStore(store: TokenStore): boolean {
|
|
const filePath = getMcpOauthStoragePath()
|
|
|
|
try {
|
|
const dir = dirname(filePath)
|
|
if (!existsSync(dir)) {
|
|
mkdirSync(dir, { recursive: true })
|
|
}
|
|
|
|
const tempPath = `${filePath}.tmp.${Date.now()}`
|
|
writeFileSync(tempPath, JSON.stringify(store, null, 2), { encoding: "utf-8", mode: 0o600 })
|
|
chmodSync(tempPath, 0o600)
|
|
renameSync(tempPath, filePath)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function loadToken(serverHost: string, resource: string): OAuthTokenData | null {
|
|
const store = readStore()
|
|
if (!store) return null
|
|
|
|
const key = buildKey(serverHost, resource)
|
|
return store[key] ?? null
|
|
}
|
|
|
|
export function saveToken(serverHost: string, resource: string, token: OAuthTokenData): boolean {
|
|
const store = readStore() ?? {}
|
|
const key = buildKey(serverHost, resource)
|
|
store[key] = token
|
|
return writeStore(store)
|
|
}
|
|
|
|
export function deleteToken(serverHost: string, resource: string): boolean {
|
|
const store = readStore()
|
|
if (!store) return true
|
|
|
|
const key = buildKey(serverHost, resource)
|
|
if (!(key in store)) {
|
|
return true
|
|
}
|
|
|
|
delete store[key]
|
|
|
|
if (Object.keys(store).length === 0) {
|
|
try {
|
|
const filePath = getMcpOauthStoragePath()
|
|
if (existsSync(filePath)) {
|
|
unlinkSync(filePath)
|
|
}
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return writeStore(store)
|
|
}
|
|
|
|
export function listTokensByHost(serverHost: string): TokenStore {
|
|
const store = readStore()
|
|
if (!store) return {}
|
|
|
|
const host = normalizeHost(serverHost)
|
|
const prefix = `${host}/`
|
|
const result: TokenStore = {}
|
|
|
|
for (const [key, value] of Object.entries(store)) {
|
|
if (key.startsWith(prefix)) {
|
|
result[key] = value
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
export function listAllTokens(): TokenStore {
|
|
return readStore() ?? {}
|
|
}
|