* XAA silent refresh: cached id_token → Layer-2 exchange → new access_token. * No browser. * * Returns undefined if the id_token is gone from cache — caller treats this * as needs-interactive-reauth (transport will 401, CC surfaces it). * * On exchange failure, clears the id_token
()
| 1749 | * process boundaries". Mirror refreshAuthorization()'s lockfile pattern. |
| 1750 | */ |
| 1751 | private async xaaRefresh(): Promise<OAuthTokens | undefined> { |
| 1752 | const idp = getXaaIdpSettings() |
| 1753 | if (!idp) return undefined // config was removed mid-session |
| 1754 | |
| 1755 | const idToken = getCachedIdpIdToken(idp.issuer) |
| 1756 | if (!idToken) { |
| 1757 | logMCPDebug( |
| 1758 | this.serverName, |
| 1759 | 'XAA: id_token not cached, needs interactive re-auth', |
| 1760 | ) |
| 1761 | return undefined |
| 1762 | } |
| 1763 | |
| 1764 | const clientId = this.serverConfig.oauth?.clientId |
| 1765 | const clientConfig = getMcpClientConfig(this.serverName, this.serverConfig) |
| 1766 | if (!clientId || !clientConfig?.clientSecret) { |
| 1767 | logMCPDebug( |
| 1768 | this.serverName, |
| 1769 | 'XAA: missing clientId or clientSecret in config — skipping silent refresh', |
| 1770 | ) |
| 1771 | return undefined // shouldn't happen if `mcp add` was correct |
| 1772 | } |
| 1773 | |
| 1774 | const idpClientSecret = getIdpClientSecret(idp.issuer) |
| 1775 | |
| 1776 | // Discover IdP token endpoint. Could cache (fetchCache.ts already |
| 1777 | // caches /.well-known/ requests), but OIDC metadata is cheap + idempotent. |
| 1778 | // xaaRefresh is the silent tokens() path — soft-fail to undefined so the |
| 1779 | // caller falls through to needs-authentication instead of throwing mid-connect. |
| 1780 | let oidc |
| 1781 | try { |
| 1782 | oidc = await discoverOidc(idp.issuer) |
| 1783 | } catch (e) { |
| 1784 | logMCPDebug( |
| 1785 | this.serverName, |
| 1786 | `XAA: OIDC discovery failed in silent refresh: ${errorMessage(e)}`, |
| 1787 | ) |
| 1788 | return undefined |
| 1789 | } |
| 1790 | |
| 1791 | try { |
| 1792 | const tokens = await performCrossAppAccess( |
| 1793 | this.serverConfig.url, |
| 1794 | { |
| 1795 | clientId, |
| 1796 | clientSecret: clientConfig.clientSecret, |
| 1797 | idpClientId: idp.clientId, |
| 1798 | idpClientSecret, |
| 1799 | idpIdToken: idToken, |
| 1800 | idpTokenEndpoint: oidc.token_endpoint, |
| 1801 | }, |
| 1802 | this.serverName, |
| 1803 | ) |
| 1804 | // Write directly (not via saveTokens) so clientId + clientSecret land in |
| 1805 | // storage even when this is the first write for serverKey. saveTokens |
| 1806 | // only spreads existing data; if no prior performMCPXaaAuth ran, |
| 1807 | // revokeServerTokens would later read tokenData.clientId as undefined |
| 1808 | // and send a client_id-less RFC 7009 request that strict ASes reject. |
no test coverage detected