@@ -38,6 +38,12 @@ export interface DetectionResult {
3838 mcp : string [ ] ;
3939 } ;
4040 apiSchema ?: { url : string ; format : "openapi" ; version ?: string } ;
41+ /** How to authenticate — the bet. Aggregated across REST, MCP, and PRM. */
42+ auth ?: {
43+ rest ?: ReadonlyArray < { name : string ; type : string ; scheme ?: string ; in ?: string ; bearerFormat ?: string ; openIdConnectUrl ?: string ; flows ?: unknown } > ;
44+ mcp ?: ReadonlyArray < { url : string ; type ?: string ; authorizationServer ?: string } > ;
45+ oauthProtectedResource ?: { authorizationServers : string [ ] ; scopes ?: string [ ] } ;
46+ } ;
4147 mcp : McpDetection [ ] ;
4248 agentCard ?: { name ?: string ; url ?: string } ;
4349 agentSkills ?: { count : number ; names : string [ ] } ;
@@ -130,24 +136,52 @@ async function checkLlmsTxt(fetchImpl: FetchLike, domain: string): Promise<boole
130136 return hit . text . length > 0 && ! / < ! d o c t y p e | < h t m l / i. test ( hit . text . slice ( 0 , 200 ) ) ;
131137}
132138
133- /** Probe conventional live-spec paths; trust the OpenAPI content-type / key. */
139+ /** Summarize the OpenAPI `securitySchemes` — how to authenticate to the API. */
140+ function extractRestAuth ( doc : any ) {
141+ const schemes = doc ?. components ?. securitySchemes ?? doc ?. securityDefinitions ; // OAS3 / Swagger2
142+ if ( ! schemes || typeof schemes !== "object" ) return undefined ;
143+ const out = Object . entries < any > ( schemes ) . map ( ( [ name , s ] ) => ( {
144+ name,
145+ type : s ?. type ,
146+ scheme : s ?. scheme ,
147+ in : s ?. in ,
148+ bearerFormat : s ?. bearerFormat ,
149+ openIdConnectUrl : s ?. openIdConnectUrl ,
150+ flows : s ?. flows ,
151+ } ) ) ;
152+ return out . length ? out : undefined ;
153+ }
154+
155+ /** Probe conventional live-spec paths; parse the OpenAPI to also surface auth. */
134156async function checkApiSchema ( fetchImpl : FetchLike , domain : string ) {
135157 const paths = [ "/api/schema/" , "/openapi.json" , "/swagger.json" , "/api/openapi.json" , "/v1/openapi.json" ] ;
136158 for ( const p of paths ) {
137159 const hit = await get ( fetchImpl , `https://${ domain } ${ p } ` ) ;
138160 if ( ! hit || ! hit . res . ok ) continue ;
139161 const ct = hit . res . headers . get ( "content-type" ) ?? "" ;
140- if ( / o p e n a p i / i. test ( ct ) ) {
141- return { url : `https://${ domain } ${ p } ` , format : "openapi" as const } ;
142- }
143162 const doc = asJson ( hit . text , ct ) ;
144- if ( doc && ( doc . openapi || doc . swagger ) ) {
145- return { url : `https://${ domain } ${ p } ` , format : "openapi" as const , version : doc . openapi ?? doc . swagger } ;
146- }
163+ const isOpenapi = / o p e n a p i / i. test ( ct ) || Boolean ( doc && ( doc . openapi || doc . swagger ) ) ;
164+ if ( ! isOpenapi ) continue ;
165+ return {
166+ url : `https://${ domain } ${ p } ` ,
167+ format : "openapi" as const ,
168+ version : doc ?. openapi ?? doc ?. swagger ,
169+ rest : extractRestAuth ( doc ) ,
170+ } ;
147171 }
148172 return undefined ;
149173}
150174
175+ /** OAuth 2.0 Protected Resource Metadata (RFC 9728) — the API's auth servers. */
176+ async function checkOauthProtectedResource ( fetchImpl : FetchLike , domain : string ) {
177+ const hit = await get ( fetchImpl , `https://${ domain } /.well-known/oauth-protected-resource` ) ;
178+ if ( ! hit || ! hit . res . ok ) return undefined ;
179+ const doc = asJson ( hit . text , hit . res . headers . get ( "content-type" ) ) ;
180+ const servers = doc ?. authorization_servers ;
181+ if ( ! Array . isArray ( servers ) || servers . length === 0 ) return undefined ;
182+ return { authorizationServers : servers as string [ ] , scopes : doc ?. scopes_supported as string [ ] | undefined } ;
183+ }
184+
151185/**
152186 * MCP self-onboarding: initialize → WWW-Authenticate → PRM → AS metadata →
153187 * registration_endpoint (DCR) + client_id_metadata_document_supported (CIMD).
@@ -181,15 +215,22 @@ async function detectMcpOnboarding(fetchImpl: FetchLike, mcpUrl: string): Promis
181215
182216export async function detect ( domain : string , fetchImpl : FetchLike = fetch ) : Promise < DetectionResult > {
183217 const errors : string [ ] = [ ] ;
184- const [ apiCatalog , serverCard , agentCard , agentSkills , llmsTxt , apiSchema ] = await Promise . all ( [
218+ const [ apiCatalog , serverCard , agentCard , agentSkills , llmsTxt , apiSchemaRaw , oauthPR ] = await Promise . all ( [
185219 checkApiCatalog ( fetchImpl , domain ) . catch ( ( e ) => ( errors . push ( `api-catalog: ${ e } ` ) , undefined ) ) ,
186220 checkServerCard ( fetchImpl , domain ) . catch ( ( e ) => ( errors . push ( `server-card: ${ e } ` ) , undefined ) ) ,
187221 checkAgentCard ( fetchImpl , domain ) . catch ( ( e ) => ( errors . push ( `agent-card: ${ e } ` ) , undefined ) ) ,
188222 checkAgentSkills ( fetchImpl , domain ) . catch ( ( e ) => ( errors . push ( `agent-skills: ${ e } ` ) , undefined ) ) ,
189223 checkLlmsTxt ( fetchImpl , domain ) . catch ( ( ) => false ) ,
190224 checkApiSchema ( fetchImpl , domain ) . catch ( ( ) => undefined ) ,
225+ checkOauthProtectedResource ( fetchImpl , domain ) . catch ( ( ) => undefined ) ,
191226 ] ) ;
192227
228+ // Keep apiSchema as the spec locator; lift its auth into the aggregate below.
229+ const restAuth = apiSchemaRaw ?. rest ;
230+ const apiSchema = apiSchemaRaw
231+ ? { url : apiSchemaRaw . url , format : apiSchemaRaw . format , version : apiSchemaRaw . version }
232+ : undefined ;
233+
193234 // Collect MCP endpoints from the server card + api-catalog, then probe each
194235 // for self-onboarding capability.
195236 const mcpSeen = new Map < string , McpDetection > ( ) ;
@@ -199,6 +240,18 @@ export async function detect(domain: string, fetchImpl: FetchLike = fetch): Prom
199240 [ ...mcpSeen . values ( ) ] . map ( async ( m ) => ( { ...m , ...( await detectMcpOnboarding ( fetchImpl , m . url ) . catch ( ( ) => ( { } ) ) ) } ) ) ,
200241 ) ;
201242
243+ // Aggregate auth — the bet — across REST (OpenAPI securitySchemes), MCP
244+ // (oauth2 + authorization server), and the PRM well-known.
245+ const mcpAuth = mcp
246+ . filter ( ( m ) => m . auth && m . auth !== "none" )
247+ . map ( ( m ) => ( { url : m . url , type : m . auth , authorizationServer : m . authorizationServer } ) ) ;
248+ const auth = {
249+ ...( restAuth ? { rest : restAuth } : { } ) ,
250+ ...( mcpAuth . length ? { mcp : mcpAuth } : { } ) ,
251+ ...( oauthPR ? { oauthProtectedResource : oauthPR } : { } ) ,
252+ } ;
253+ const hasAuth = Object . keys ( auth ) . length > 0 ;
254+
202255 const found : string [ ] = [ ] ;
203256 if ( apiCatalog ) found . push ( "api-catalog" ) ;
204257 if ( apiSchema ) found . push ( "openapi-schema" ) ;
@@ -207,6 +260,7 @@ export async function detect(domain: string, fetchImpl: FetchLike = fetch): Prom
207260 if ( agentCard ) found . push ( "agent-card" ) ;
208261 if ( agentSkills ) found . push ( "agent-skills" ) ;
209262 if ( llmsTxt ) found . push ( "llms.txt" ) ;
263+ if ( hasAuth ) found . push ( "auth" ) ;
210264
211- return { domain, found, apiCatalog, apiSchema, mcp, agentCard, agentSkills, llmsTxt, errors } ;
265+ return { domain, found, apiCatalog, apiSchema, auth : hasAuth ? auth : undefined , mcp, agentCard, agentSkills, llmsTxt, errors } ;
212266}
0 commit comments