import { type IonApiConfig, getTokenEndpoint } from "../config/ionapi.js";
import { logger } from "../utils/logger.js";
interface TokenResponse {
access_token: string;
token_type: string;
expires_in: number;
scope?: string;
}
interface TokenCache {
accessToken: string;
expiresAt: number;
}
// Token cache per environment
const tokenCache = new Map<string, TokenCache>();
// Buffer time before token expiry (5 minutes)
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
/**
* Get a valid access token for the given ION API configuration
* Automatically refreshes if expired or about to expire
*/
export async function getAccessToken(config: IonApiConfig): Promise<string> {
const cacheKey = config.ti; // Use tenant ID as cache key
// Check if we have a valid cached token
const cached = tokenCache.get(cacheKey);
if (cached && Date.now() < cached.expiresAt - TOKEN_EXPIRY_BUFFER_MS) {
logger.debug("Using cached access token");
return cached.accessToken;
}
// Request new token
logger.info("Requesting new access token");
const token = await requestNewToken(config);
// Cache the token
tokenCache.set(cacheKey, {
accessToken: token.access_token,
expiresAt: Date.now() + token.expires_in * 1000,
});
return token.access_token;
}
/**
* Request a new access token from the ION API
*/
async function requestNewToken(config: IonApiConfig): Promise<TokenResponse> {
const tokenEndpoint = getTokenEndpoint(config);
// Build the request body for service account authentication
const body = new URLSearchParams({
grant_type: "password",
username: config.saak,
password: config.sask,
client_id: config.ci,
client_secret: config.cs,
});
logger.debug(`Requesting token from: ${tokenEndpoint}`);
const response = await fetch(tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: body.toString(),
});
if (!response.ok) {
const errorText = await response.text();
logger.error(`Token request failed: ${response.status}`, errorText);
throw new Error(
`Failed to obtain access token: ${response.status} ${response.statusText}`
);
}
const tokenResponse = (await response.json()) as TokenResponse;
logger.info(
`Access token obtained, expires in ${tokenResponse.expires_in} seconds`
);
return tokenResponse;
}
/**
* Clear the token cache for a specific environment or all environments
*/
export function clearTokenCache(tenantId?: string): void {
if (tenantId) {
tokenCache.delete(tenantId);
} else {
tokenCache.clear();
}
logger.info("Token cache cleared");
}
/**
* Check if a valid token is cached for the given configuration
*/
export function hasValidToken(config: IonApiConfig): boolean {
const cached = tokenCache.get(config.ti);
return cached !== undefined && Date.now() < cached.expiresAt - TOKEN_EXPIRY_BUFFER_MS;
}