using StellaOps.Auth.Client; using StellaOps.Cli.Services; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Cli.Extensions; /// /// Extension methods for IStellaOpsTokenClient providing compatibility with older CLI patterns. /// These bridge the gap between the old API (GetTokenAsync, GetAccessTokenAsync) and the /// new API (RequestClientCredentialsTokenAsync, GetCachedTokenAsync). /// public static class StellaOpsTokenClientExtensions { /// /// Requests an access token using client credentials flow with the specified scopes. /// This is a compatibility shim for the old GetAccessTokenAsync pattern. /// public static async Task GetAccessTokenAsync( this IStellaOpsTokenClient client, IEnumerable scopes, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(client); var scope = scopes is not null ? string.Join(" ", scopes.Where(s => !string.IsNullOrWhiteSpace(s))) : null; return await client.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false); } /// /// Requests an access token using client credentials flow with a single scope. /// public static async Task GetAccessTokenAsync( this IStellaOpsTokenClient client, string scope, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(client); return await client.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false); } /// /// Gets a cached access token or requests a new one if not cached or expired. /// This is a compatibility shim for the old GetCachedAccessTokenAsync pattern. /// Cache key includes effective tenant to prevent cross-tenant cache collisions. /// public static async Task GetCachedAccessTokenAsync( this IStellaOpsTokenClient client, IEnumerable scopes, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(client); var scopeList = scopes?.Where(s => !string.IsNullOrWhiteSpace(s)).OrderBy(s => s).ToArray() ?? []; var scope = string.Join(" ", scopeList); var cacheKey = BuildCacheKey(scope); // Check cache first var cached = await client.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (cached is not null && !cached.IsExpired(TimeProvider.System, TimeSpan.FromMinutes(1))) { return cached; } // Request new token var result = await client.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false); var entry = result.ToCacheEntry(); // Cache the result await client.CacheTokenAsync(cacheKey, entry, cancellationToken).ConfigureAwait(false); return entry; } /// /// Gets a cached access token or requests a new one if not cached or expired. /// Single scope version. /// public static async Task GetCachedAccessTokenAsync( this IStellaOpsTokenClient client, string scope, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(client); var cacheKey = BuildCacheKey(scope ?? "default"); // Check cache first var cached = await client.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (cached is not null && !cached.IsExpired(TimeProvider.System, TimeSpan.FromMinutes(1))) { return cached; } // Request new token var result = await client.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false); var entry = result.ToCacheEntry(); // Cache the result await client.CacheTokenAsync(cacheKey, entry, cancellationToken).ConfigureAwait(false); return entry; } /// /// Requests a token using client credentials. Parameterless version for simple cases. /// public static async Task GetTokenAsync( this IStellaOpsTokenClient client, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(client); return await client.RequestClientCredentialsTokenAsync(null, null, cancellationToken).ConfigureAwait(false); } /// /// Builds a cache key that includes the effective tenant to prevent cross-tenant /// token cache collisions when users switch tenants between CLI invocations. /// private static string BuildCacheKey(string scope) { var tenant = TenantProfileStore.GetEffectiveTenant(null) ?? "none"; return $"cc:{tenant}:{scope}"; } }