()
| 1538 | } |
| 1539 | |
| 1540 | async tokens(): Promise<OAuthTokens | undefined> { |
| 1541 | // Cross-process token changes (another CC instance refreshed or invalidated) |
| 1542 | // are picked up via the keychain cache TTL (see macOsKeychainStorage.ts). |
| 1543 | // In-process writes already invalidate the cache via storage.update(). |
| 1544 | // We do NOT clearKeychainCache() here — tokens() is called by the MCP SDK's |
| 1545 | // _commonHeaders on every request, and forcing a cache miss would trigger |
| 1546 | // a blocking spawnSync(`security find-generic-password`) 30-40x/sec. |
| 1547 | // See CPU profile: spawnSync was 7.2% of total CPU after PR #19436. |
| 1548 | const storage = getSecureStorage() |
| 1549 | const data = await storage.readAsync() |
| 1550 | const serverKey = getServerKey(this.serverName, this.serverConfig) |
| 1551 | |
| 1552 | const tokenData = data?.mcpOAuth?.[serverKey] |
| 1553 | |
| 1554 | // XAA: a cached id_token plays the same UX role as a refresh_token — run |
| 1555 | // the silent exchange to get a fresh access_token without a browser. The |
| 1556 | // id_token does expire (we re-acquire via `xaa login` when it does); the |
| 1557 | // point is that while it's valid, re-auth is zero-interaction. |
| 1558 | // |
| 1559 | // Only fire when we don't have a refresh_token. If the AS returned one, |
| 1560 | // the normal refresh path (below) is cheaper — 1 request vs the 4-request |
| 1561 | // XAA chain. If that refresh is revoked, refreshAuthorization() clears it |
| 1562 | // (invalidateCredentials('tokens')), and the next tokens() falls through |
| 1563 | // to here. |
| 1564 | // |
| 1565 | // Fires on: |
| 1566 | // - never authed (!tokenData) → first connect, auto-auth |
| 1567 | // - SDK partial write {accessToken:''} → stale from past session |
| 1568 | // - expired/expiring, no refresh_token → proactive XAA re-auth |
| 1569 | // |
| 1570 | // No special-casing of {accessToken:'', expiresAt:0}. Yes, SDK auth() |
| 1571 | // writes that mid-flow (saveClientInformation defaults). But with this |
| 1572 | // auto-auth branch, the *first* tokens() call — before auth() writes |
| 1573 | // anything — fires xaaRefresh. If id_token is cached, SDK short-circuits |
| 1574 | // there and never reaches the write. If id_token isn't cached, xaaRefresh |
| 1575 | // returns undefined in ~1 keychain read, auth() proceeds, writes the |
| 1576 | // marker, calls tokens() again, xaaRefresh fails again identically. |
| 1577 | // Harmless redundancy, not a wasted exchange. And guarding on `!==''` |
| 1578 | // permanently bricks auto-auth when a *prior* session left that marker |
| 1579 | // in keychain — real bug seen with xaa.dev. |
| 1580 | // |
| 1581 | // xaaRefresh() internally short-circuits to undefined when the id_token |
| 1582 | // isn't cached (or settings.xaaIdp is gone) → we fall through to the |
| 1583 | // existing needs-auth path → user runs `xaa login`. |
| 1584 | // |
| 1585 | if ( |
| 1586 | isXaaEnabled() && |
| 1587 | this.serverConfig.oauth?.xaa && |
| 1588 | !tokenData?.refreshToken && |
| 1589 | (!tokenData?.accessToken || |
| 1590 | (tokenData.expiresAt - Date.now()) / 1000 <= 300) |
| 1591 | ) { |
| 1592 | if (!this._refreshInProgress) { |
| 1593 | logMCPDebug( |
| 1594 | this.serverName, |
| 1595 | tokenData |
| 1596 | ? `XAA: access_token expiring, attempting silent exchange` |
| 1597 | : `XAA: no access_token yet, attempting silent exchange`, |
no test coverage detected