// Cache for pending token requests to avoid concurrent requests
const pendingTokenRequests = new Map();
const tokenCache = new Map();
// Configuration for cache management
const MAX_CACHE_SIZE = 100; // Maximum number of tokens to cache
// Track last access time for LRU eviction
const lastAccessTime = new Map();
/**
 * Parses a JWT token to extract the expiration time
 * @param token - The JWT token string
 * @returns The expiration time as a Unix timestamp in milliseconds, or null if not found/invalid
 */
function getTokenExpiration(token) {
    try {
        const parts = token.split(".");
        if (parts.length === 3 && parts[1]) {
            const payload = JSON.parse(Buffer.from(parts[1], "base64").toString());
            if (payload.exp) {
                // Convert from seconds to milliseconds
                return payload.exp * 1000;
            }
        }
    }
    catch {
        // Silently handle JWT parsing errors
    }
    return null;
}
/**
 * Checks if a token is still valid (not expired)
 * @param expiresAt - The expiration timestamp in milliseconds
 * @returns true if the token is still valid, false otherwise
 */
function isTokenValid(expiresAt) {
    // Add a 60 second buffer to account for clock skew and network delays
    const bufferMs = 60 * 1000;
    return Date.now() < expiresAt - bufferMs;
}
/**
 * Enforces cache size limit by evicting least recently used tokens
 */
function enforceCacheSizeLimit() {
    if (tokenCache.size <= MAX_CACHE_SIZE) {
        return;
    }
    // Sort by last access time (oldest first)
    const entries = Array.from(tokenCache.keys()).map((key) => ({
        key,
        lastAccess: lastAccessTime.get(key) ?? 0,
    }));
    entries.sort((a, b) => a.lastAccess - b.lastAccess);
    // Remove oldest entries until we're under the limit
    const toRemove = entries.slice(0, entries.length - MAX_CACHE_SIZE);
    for (const { key } of toRemove) {
        tokenCache.delete(key);
        lastAccessTime.delete(key);
    }
}
/**
 * Gets a scoped token with apps:update scope
 * Uses both a token cache (for reuse) and a promise cache (to prevent concurrent requests)
 *
 * @param applicationId - The application ID
 * @param superblocksBaseUrl - The Superblocks base URL
 * @param jwt - The JWT token for authentication
 * @returns A promise that resolves to the scoped token
 */
export async function getScopedToken({ applicationId, superblocksBaseUrl, jwt, }) {
    // First, check if we have a valid cached token
    const cached = tokenCache.get(applicationId);
    if (cached && isTokenValid(cached.expiresAt)) {
        // Update last access time
        lastAccessTime.set(applicationId, Date.now());
        return cached.token;
    }
    // Remove expired token if present
    if (cached) {
        tokenCache.delete(applicationId);
        lastAccessTime.delete(applicationId);
    }
    // Check if there's already a pending request for this application
    const pendingRequest = pendingTokenRequests.get(applicationId);
    if (pendingRequest) {
        return pendingRequest;
    }
    // Create a new token request and cache the promise
    const tokenPromise = (async () => {
        try {
            const tokenUrl = `${superblocksBaseUrl}/api/v1/token/scoped`;
            const tokenResponse = await fetch(tokenUrl, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    Authorization: `Bearer ${jwt}`,
                },
                body: JSON.stringify({
                    applicationId,
                    scope: "apps:update",
                }),
            });
            if (!tokenResponse.ok) {
                const errorText = await tokenResponse
                    .text()
                    .catch(() => "Unable to read error response");
                throw new Error(`Failed to get scoped token: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`);
            }
            const tokenData = (await tokenResponse.json());
            if (!tokenData?.access_token) {
                throw new Error("Failed to get scoped token");
            }
            const token = tokenData.access_token;
            // Cache the token with its expiration time
            const expiresAt = getTokenExpiration(token);
            if (expiresAt) {
                tokenCache.set(applicationId, { token, expiresAt });
                lastAccessTime.set(applicationId, Date.now());
                // Enforce cache size limit
                enforceCacheSizeLimit();
            }
            return token;
        }
        finally {
            // Remove from cache once the request completes (success or failure)
            pendingTokenRequests.delete(applicationId);
        }
    })();
    // Cache the promise so concurrent calls can share it
    pendingTokenRequests.set(applicationId, tokenPromise);
    return tokenPromise;
}
//# sourceMappingURL=scoped-token-utils.js.map