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,183 @@ 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 updateTokens = Effect . fn ( "McpAuth.updateTokens" ) ( function * (
92+ mcpName : string ,
93+ tokens : Tokens ,
94+ serverUrl ?: string ,
95+ ) {
96+ const entry = ( yield * get ( mcpName ) ) ?? { }
97+ entry . tokens = tokens
98+ yield * set ( mcpName , entry , serverUrl )
99+ } )
100+
101+ const updateClientInfo = Effect . fn ( "McpAuth.updateClientInfo" ) ( function * (
102+ mcpName : string ,
103+ clientInfo : ClientInfo ,
104+ serverUrl ?: string ,
105+ ) {
106+ const entry = ( yield * get ( mcpName ) ) ?? { }
107+ entry . clientInfo = clientInfo
108+ yield * set ( mcpName , entry , serverUrl )
109+ } )
110+
111+ const updateCodeVerifier = Effect . fn ( "McpAuth.updateCodeVerifier" ) ( function * (
112+ mcpName : string ,
113+ codeVerifier : string ,
114+ ) {
115+ const entry = ( yield * get ( mcpName ) ) ?? { }
116+ entry . codeVerifier = codeVerifier
117+ yield * set ( mcpName , entry )
118+ } )
119+
120+ const clearCodeVerifier = Effect . fn ( "McpAuth.clearCodeVerifier" ) ( function * ( mcpName : string ) {
121+ const entry = yield * get ( mcpName )
122+ if ( entry ) {
123+ delete entry . codeVerifier
124+ yield * set ( mcpName , entry )
125+ }
126+ } )
127+
128+ const updateOAuthState = Effect . fn ( "McpAuth.updateOAuthState" ) ( function * ( mcpName : string , oauthState : string ) {
129+ const entry = ( yield * get ( mcpName ) ) ?? { }
130+ entry . oauthState = oauthState
131+ yield * set ( mcpName , entry )
132+ } )
133+
134+ const getOAuthState = Effect . fn ( "McpAuth.getOAuthState" ) ( function * ( mcpName : string ) {
135+ const entry = yield * get ( mcpName )
136+ return entry ?. oauthState
137+ } )
138+
139+ const clearOAuthState = Effect . fn ( "McpAuth.clearOAuthState" ) ( function * ( mcpName : string ) {
140+ const entry = yield * get ( mcpName )
141+ if ( entry ) {
142+ delete entry . oauthState
143+ yield * set ( mcpName , entry )
144+ }
145+ } )
146+
147+ const isTokenExpired = Effect . fn ( "McpAuth.isTokenExpired" ) ( function * ( mcpName : string ) {
148+ const entry = yield * get ( mcpName )
149+ if ( ! entry ?. tokens ) return null
150+ if ( ! entry . tokens . expiresAt ) return false
151+ return entry . tokens . expiresAt < Date . now ( ) / 1000
152+ } )
153+
154+ return Service . of ( {
155+ all,
156+ get,
157+ getForUrl,
158+ set,
159+ remove,
160+ updateTokens,
161+ updateClientInfo,
162+ updateCodeVerifier,
163+ clearCodeVerifier,
164+ updateOAuthState,
165+ getOAuthState,
166+ clearOAuthState,
167+ isTokenExpired,
168+ } )
169+ } ) ,
170+ )
171+
172+ const defaultLayer = layer . pipe ( Layer . provide ( AppFileSystem . defaultLayer ) )
173+
174+ const runPromise = makeRunPromise ( Service , defaultLayer )
175+
176+ // Async facades for backward compat (used by McpOAuthProvider, CLI)
177+
178+ export const get = async ( mcpName : string ) => runPromise ( ( svc ) => svc . get ( mcpName ) )
179+
180+ export const getForUrl = async ( mcpName : string , serverUrl : string ) =>
181+ runPromise ( ( svc ) => svc . getForUrl ( mcpName , serverUrl ) )
182+
183+ export const all = async ( ) => runPromise ( ( svc ) => svc . all ( ) )
184+
185+ export const set = async ( mcpName : string , entry : Entry , serverUrl ?: string ) =>
186+ runPromise ( ( svc ) => svc . set ( mcpName , entry , serverUrl ) )
187+
188+ export const remove = async ( mcpName : string ) => runPromise ( ( svc ) => svc . remove ( mcpName ) )
189+
190+ export const updateTokens = async ( mcpName : string , tokens : Tokens , serverUrl ?: string ) =>
191+ runPromise ( ( svc ) => svc . updateTokens ( mcpName , tokens , serverUrl ) )
192+
193+ export const updateClientInfo = async ( mcpName : string , clientInfo : ClientInfo , serverUrl ?: string ) =>
194+ runPromise ( ( svc ) => svc . updateClientInfo ( mcpName , clientInfo , serverUrl ) )
195+
196+ export const updateCodeVerifier = async ( mcpName : string , codeVerifier : string ) =>
197+ runPromise ( ( svc ) => svc . updateCodeVerifier ( mcpName , codeVerifier ) )
198+
199+ export const clearCodeVerifier = async ( mcpName : string ) => runPromise ( ( svc ) => svc . clearCodeVerifier ( mcpName ) )
200+
201+ export const updateOAuthState = async ( mcpName : string , oauthState : string ) =>
202+ runPromise ( ( svc ) => svc . updateOAuthState ( mcpName , oauthState ) )
203+
204+ export const getOAuthState = async ( mcpName : string ) => runPromise ( ( svc ) => svc . getOAuthState ( mcpName ) )
205+
206+ export const clearOAuthState = async ( mcpName : string ) => runPromise ( ( svc ) => svc . clearOAuthState ( mcpName ) )
207+
208+ export const isTokenExpired = async ( mcpName : string ) => runPromise ( ( svc ) => svc . isTokenExpired ( mcpName ) )
130209}
0 commit comments