11import path from "path"
22import z from "zod"
33import { Global } from "../global"
4- import { Filesystem } from "../util/filesystem"
4+ import { Effect , Layer , ServiceMap } from "effect"
5+ import { AppFileSystem } from "@/filesystem"
6+ import { makeRunPromise } from "@/effect/run-service"
57
68export namespace McpAuth {
79 export const Tokens = z . object ( {
@@ -25,106 +27,155 @@ export namespace McpAuth {
2527 clientInfo : ClientInfo . optional ( ) ,
2628 codeVerifier : z . string ( ) . optional ( ) ,
2729 oauthState : z . string ( ) . optional ( ) ,
28- serverUrl : z . string ( ) . optional ( ) , // Track the URL these credentials are for
30+ serverUrl : z . string ( ) . optional ( ) ,
2931 } )
3032 export type Entry = z . infer < typeof Entry >
3133
3234 const filepath = path . join ( Global . Path . data , "mcp-auth.json" )
3335
34- export async function get ( mcpName : string ) : Promise < Entry | undefined > {
35- const data = await all ( )
36- return data [ mcpName ]
36+ export interface Interface {
37+ readonly all : ( ) => Effect . Effect < Record < string , Entry > >
38+ readonly get : ( mcpName : string ) => Effect . Effect < Entry | undefined >
39+ readonly getForUrl : ( mcpName : string , serverUrl : string ) => Effect . Effect < Entry | undefined >
40+ readonly set : ( mcpName : string , entry : Entry , serverUrl ?: string ) => Effect . Effect < void >
41+ readonly remove : ( mcpName : string ) => Effect . Effect < void >
42+ readonly updateTokens : ( mcpName : string , tokens : Tokens , serverUrl ?: string ) => Effect . Effect < void >
43+ readonly updateClientInfo : ( mcpName : string , clientInfo : ClientInfo , serverUrl ?: string ) => Effect . Effect < void >
44+ readonly updateCodeVerifier : ( mcpName : string , codeVerifier : string ) => Effect . Effect < void >
45+ readonly clearCodeVerifier : ( mcpName : string ) => Effect . Effect < void >
46+ readonly updateOAuthState : ( mcpName : string , oauthState : string ) => Effect . Effect < void >
47+ readonly getOAuthState : ( mcpName : string ) => Effect . Effect < string | undefined >
48+ readonly clearOAuthState : ( mcpName : string ) => Effect . Effect < void >
49+ readonly isTokenExpired : ( mcpName : string ) => Effect . Effect < boolean | null >
3750 }
3851
39- /**
40- * Get auth entry and validate it's for the correct URL.
41- * Returns undefined if URL has changed (credentials are invalid).
42- */
43- export async function getForUrl ( mcpName : string , serverUrl : string ) : Promise < Entry | undefined > {
44- const entry = await get ( mcpName )
45- if ( ! entry ) return undefined
46-
47- // If no serverUrl is stored, this is from an old version - consider it invalid
48- if ( ! entry . serverUrl ) return undefined
49-
50- // If URL has changed, credentials are invalid
51- if ( entry . serverUrl !== serverUrl ) return undefined
52-
53- return entry
54- }
55-
56- export async function all ( ) : Promise < Record < string , Entry > > {
57- return Filesystem . readJson < Record < string , Entry > > ( filepath ) . catch ( ( ) => ( { } ) )
58- }
59-
60- export async function set ( mcpName : string , entry : Entry , serverUrl ?: string ) : Promise < void > {
61- const data = await all ( )
62- // Always update serverUrl if provided
63- if ( serverUrl ) {
64- entry . serverUrl = serverUrl
65- }
66- await Filesystem . writeJson ( filepath , { ...data , [ mcpName ] : entry } , 0o600 )
67- }
68-
69- export async function remove ( mcpName : string ) : Promise < void > {
70- const data = await all ( )
71- delete data [ mcpName ]
72- await Filesystem . writeJson ( filepath , data , 0o600 )
73- }
74-
75- export async function updateTokens ( mcpName : string , tokens : Tokens , serverUrl ?: string ) : Promise < void > {
76- const entry = ( await get ( mcpName ) ) ?? { }
77- entry . tokens = tokens
78- await set ( mcpName , entry , serverUrl )
79- }
80-
81- export async function updateClientInfo ( mcpName : string , clientInfo : ClientInfo , serverUrl ?: string ) : Promise < void > {
82- const entry = ( await get ( mcpName ) ) ?? { }
83- entry . clientInfo = clientInfo
84- await set ( mcpName , entry , serverUrl )
85- }
86-
87- export async function updateCodeVerifier ( mcpName : string , codeVerifier : string ) : Promise < void > {
88- const entry = ( await get ( mcpName ) ) ?? { }
89- entry . codeVerifier = codeVerifier
90- await set ( mcpName , entry )
91- }
92-
93- export async function clearCodeVerifier ( mcpName : string ) : Promise < void > {
94- const entry = await get ( mcpName )
95- if ( entry ) {
96- delete entry . codeVerifier
97- await set ( mcpName , entry )
98- }
99- }
100-
101- export async function updateOAuthState ( mcpName : string , oauthState : string ) : Promise < void > {
102- const entry = ( await get ( mcpName ) ) ?? { }
103- entry . oauthState = oauthState
104- await set ( mcpName , entry )
105- }
106-
107- export async function getOAuthState ( mcpName : string ) : Promise < string | undefined > {
108- const entry = await get ( mcpName )
109- return entry ?. oauthState
110- }
111-
112- export async function clearOAuthState ( mcpName : string ) : Promise < void > {
113- const entry = await get ( mcpName )
114- if ( entry ) {
115- delete entry . oauthState
116- await set ( mcpName , entry )
117- }
118- }
119-
120- /**
121- * Check if stored tokens are expired.
122- * Returns null if no tokens exist, false if no expiry or not expired, true if expired.
123- */
124- export async function isTokenExpired ( mcpName : string ) : Promise < boolean | null > {
125- const entry = await get ( mcpName )
126- if ( ! entry ?. tokens ) return null
127- if ( ! entry . tokens . expiresAt ) return false
128- return entry . tokens . expiresAt < Date . now ( ) / 1000
129- }
52+ export class Service extends ServiceMap . Service < Service , Interface > ( ) ( "@opencode/McpAuth" ) { }
53+
54+ export const layer = Layer . effect (
55+ Service ,
56+ Effect . gen ( function * ( ) {
57+ const fs = yield * AppFileSystem . Service
58+
59+ const all = Effect . fn ( "McpAuth.all" ) ( function * ( ) {
60+ return yield * fs . readJson ( filepath ) . pipe (
61+ Effect . map ( ( data ) => data as Record < string , Entry > ) ,
62+ Effect . catch ( ( ) => Effect . succeed ( { } as Record < string , Entry > ) ) ,
63+ )
64+ } )
65+
66+ const get = Effect . fn ( "McpAuth.get" ) ( function * ( mcpName : string ) {
67+ const data = yield * all ( )
68+ return data [ mcpName ]
69+ } )
70+
71+ const getForUrl = Effect . fn ( "McpAuth.getForUrl" ) ( function * ( mcpName : string , serverUrl : string ) {
72+ const entry = yield * get ( mcpName )
73+ if ( ! entry ) return undefined
74+ if ( ! entry . serverUrl ) return undefined
75+ if ( entry . serverUrl !== serverUrl ) return undefined
76+ return entry
77+ } )
78+
79+ const set = Effect . fn ( "McpAuth.set" ) ( function * ( mcpName : string , entry : Entry , serverUrl ?: string ) {
80+ const data = yield * all ( )
81+ if ( serverUrl ) entry . serverUrl = serverUrl
82+ yield * fs . writeJson ( filepath , { ...data , [ mcpName ] : entry } , 0o600 ) . pipe ( Effect . orDie )
83+ } )
84+
85+ const remove = Effect . fn ( "McpAuth.remove" ) ( function * ( mcpName : string ) {
86+ const data = yield * all ( )
87+ delete data [ mcpName ]
88+ yield * fs . writeJson ( filepath , data , 0o600 ) . pipe ( Effect . orDie )
89+ } )
90+
91+ const updateField = < K extends keyof Entry > ( field : K , spanName : string ) =>
92+ Effect . fn ( `McpAuth.${ spanName } ` ) ( function * ( mcpName : string , value : NonNullable < Entry [ K ] > , serverUrl ?: string ) {
93+ const entry = ( yield * get ( mcpName ) ) ?? { }
94+ entry [ field ] = value
95+ yield * set ( mcpName , entry , serverUrl )
96+ } )
97+
98+ const clearField = < K extends keyof Entry > ( field : K , spanName : string ) =>
99+ Effect . fn ( `McpAuth.${ spanName } ` ) ( function * ( mcpName : string ) {
100+ const entry = yield * get ( mcpName )
101+ if ( entry ) {
102+ delete entry [ field ]
103+ yield * set ( mcpName , entry )
104+ }
105+ } )
106+
107+ const updateTokens = updateField ( "tokens" , "updateTokens" )
108+ const updateClientInfo = updateField ( "clientInfo" , "updateClientInfo" )
109+ const updateCodeVerifier = updateField ( "codeVerifier" , "updateCodeVerifier" )
110+ const updateOAuthState = updateField ( "oauthState" , "updateOAuthState" )
111+ const clearCodeVerifier = clearField ( "codeVerifier" , "clearCodeVerifier" )
112+ const clearOAuthState = clearField ( "oauthState" , "clearOAuthState" )
113+
114+ const getOAuthState = Effect . fn ( "McpAuth.getOAuthState" ) ( function * ( mcpName : string ) {
115+ const entry = yield * get ( mcpName )
116+ return entry ?. oauthState
117+ } )
118+
119+ const isTokenExpired = Effect . fn ( "McpAuth.isTokenExpired" ) ( function * ( mcpName : string ) {
120+ const entry = yield * get ( mcpName )
121+ if ( ! entry ?. tokens ) return null
122+ if ( ! entry . tokens . expiresAt ) return false
123+ return entry . tokens . expiresAt < Date . now ( ) / 1000
124+ } )
125+
126+ return Service . of ( {
127+ all,
128+ get,
129+ getForUrl,
130+ set,
131+ remove,
132+ updateTokens,
133+ updateClientInfo,
134+ updateCodeVerifier,
135+ clearCodeVerifier,
136+ updateOAuthState,
137+ getOAuthState,
138+ clearOAuthState,
139+ isTokenExpired,
140+ } )
141+ } ) ,
142+ )
143+
144+ const defaultLayer = layer . pipe ( Layer . provide ( AppFileSystem . defaultLayer ) )
145+
146+ const runPromise = makeRunPromise ( Service , defaultLayer )
147+
148+ // Async facades for backward compat (used by McpOAuthProvider, CLI)
149+
150+ export const get = async ( mcpName : string ) => runPromise ( ( svc ) => svc . get ( mcpName ) )
151+
152+ export const getForUrl = async ( mcpName : string , serverUrl : string ) =>
153+ runPromise ( ( svc ) => svc . getForUrl ( mcpName , serverUrl ) )
154+
155+ export const all = async ( ) => runPromise ( ( svc ) => svc . all ( ) )
156+
157+ export const set = async ( mcpName : string , entry : Entry , serverUrl ?: string ) =>
158+ runPromise ( ( svc ) => svc . set ( mcpName , entry , serverUrl ) )
159+
160+ export const remove = async ( mcpName : string ) => runPromise ( ( svc ) => svc . remove ( mcpName ) )
161+
162+ export const updateTokens = async ( mcpName : string , tokens : Tokens , serverUrl ?: string ) =>
163+ runPromise ( ( svc ) => svc . updateTokens ( mcpName , tokens , serverUrl ) )
164+
165+ export const updateClientInfo = async ( mcpName : string , clientInfo : ClientInfo , serverUrl ?: string ) =>
166+ runPromise ( ( svc ) => svc . updateClientInfo ( mcpName , clientInfo , serverUrl ) )
167+
168+ export const updateCodeVerifier = async ( mcpName : string , codeVerifier : string ) =>
169+ runPromise ( ( svc ) => svc . updateCodeVerifier ( mcpName , codeVerifier ) )
170+
171+ export const clearCodeVerifier = async ( mcpName : string ) => runPromise ( ( svc ) => svc . clearCodeVerifier ( mcpName ) )
172+
173+ export const updateOAuthState = async ( mcpName : string , oauthState : string ) =>
174+ runPromise ( ( svc ) => svc . updateOAuthState ( mcpName , oauthState ) )
175+
176+ export const getOAuthState = async ( mcpName : string ) => runPromise ( ( svc ) => svc . getOAuthState ( mcpName ) )
177+
178+ export const clearOAuthState = async ( mcpName : string ) => runPromise ( ( svc ) => svc . clearOAuthState ( mcpName ) )
179+
180+ export const isTokenExpired = async ( mcpName : string ) => runPromise ( ( svc ) => svc . isTokenExpired ( mcpName ) )
130181}
0 commit comments