( serverUrl: string, config: XaaConfig, serverName = 'xaa', abortSignal?: AbortSignal, )
| 424 | * @param serverName Server name for debug logging |
| 425 | */ |
| 426 | export async function performCrossAppAccess( |
| 427 | serverUrl: string, |
| 428 | config: XaaConfig, |
| 429 | serverName = 'xaa', |
| 430 | abortSignal?: AbortSignal, |
| 431 | ): Promise<XaaResult> { |
| 432 | const fetchFn = makeXaaFetch(abortSignal) |
| 433 | |
| 434 | logMCPDebug(serverName, `XAA: discovering PRM for ${serverUrl}`) |
| 435 | const prm = await discoverProtectedResource(serverUrl, { fetchFn }) |
| 436 | logMCPDebug( |
| 437 | serverName, |
| 438 | `XAA: discovered resource=${prm.resource} ASes=[${prm.authorization_servers.join(', ')}]`, |
| 439 | ) |
| 440 | |
| 441 | // Try each advertised AS in order. grant_types_supported is OPTIONAL per |
| 442 | // RFC 8414 §2 — only skip if the AS explicitly advertises a list that omits |
| 443 | // jwt-bearer. If absent, let the token endpoint decide. |
| 444 | let asMeta: AuthorizationServerMetadata | undefined |
| 445 | const asErrors: string[] = [] |
| 446 | for (const asUrl of prm.authorization_servers) { |
| 447 | let candidate: AuthorizationServerMetadata |
| 448 | try { |
| 449 | candidate = await discoverAuthorizationServer(asUrl, { fetchFn }) |
| 450 | } catch (e) { |
| 451 | if (abortSignal?.aborted) throw e |
| 452 | asErrors.push(`${asUrl}: ${e instanceof Error ? e.message : String(e)}`) |
| 453 | continue |
| 454 | } |
| 455 | if ( |
| 456 | candidate.grant_types_supported && |
| 457 | !candidate.grant_types_supported.includes(JWT_BEARER_GRANT) |
| 458 | ) { |
| 459 | asErrors.push( |
| 460 | `${asUrl}: does not advertise jwt-bearer grant (supported: ${candidate.grant_types_supported.join(', ')})`, |
| 461 | ) |
| 462 | continue |
| 463 | } |
| 464 | asMeta = candidate |
| 465 | break |
| 466 | } |
| 467 | if (!asMeta) { |
| 468 | throw new Error( |
| 469 | `XAA: no authorization server supports jwt-bearer. Tried: ${asErrors.join('; ')}`, |
| 470 | ) |
| 471 | } |
| 472 | // Pick auth method from what the AS advertises. We handle |
| 473 | // client_secret_basic and client_secret_post; if the AS only supports post, |
| 474 | // honor that, else default to basic (SEP-990 conformance expectation). |
| 475 | const authMethods = asMeta.token_endpoint_auth_methods_supported |
| 476 | const authMethod: 'client_secret_basic' | 'client_secret_post' = |
| 477 | authMethods && |
| 478 | !authMethods.includes('client_secret_basic') && |
| 479 | authMethods.includes('client_secret_post') |
| 480 | ? 'client_secret_post' |
| 481 | : 'client_secret_basic' |
| 482 | logMCPDebug( |
| 483 | serverName, |
no test coverage detected