import { getCachedMMToken, setCachedMMToken, } from "../../utils/local-data-store/jobs-shared-data.js"; import { HTTPClient, HTTPClientFactory } from "../../utils/http-client.js"; import { CoreConfiguration } from "../data-sync-service/configuration-manager.js"; import { logger } from "../reporting-service/index.js"; /** * Manages the MM API token with caching and expiration checking * Tokens are cached across jobs and job instances in SQLite runtime state (jobs-shared-data.ts). * @jest-environment node */ export class MMTokenManager { private token: string; private tokenExpiration: Date | null; private baseUrl: string; private api: HTTPClient; constructor() { const cached = getCachedMMToken(); this.token = cached?.token || ""; this.tokenExpiration = cached?.expiration ? new Date(cached.expiration) : null; const config = CoreConfiguration.inst(); this.baseUrl = config.mmApiBaseUrl; this.api = HTTPClientFactory.getInstance({ baseUrl: this.baseUrl, retryAttempts: config.mmApiRetryAttempts, timeoutMs: config.mmApiTimeoutSeconds * 1000, }); } /** * Checks if the current token is expired or will expire within the next minute * @returns boolean indicating if token needs refresh */ private isTokenExpired(): boolean { if (!this.tokenExpiration) return true; // Add 1 minute buffer to ensure we refresh before actual expiration const bufferTimeMS = 60_000; const isExpired = this.tokenExpiration.getTime() - bufferTimeMS <= Date.now(); if (isExpired) { logger.info("isTokenExpired: the api token from MM has expired"); } return isExpired; } /** * Safely extracts expiration time from JWT token * @param token JWT token string * @returns Date object representing token expiration, or null if cannot be determined */ private getTokenExpiration(token: string): Date | null { try { // Split token into parts const parts = token.split("."); if (parts.length !== 3) { console.warn("Invalid JWT format: token does not have 3 parts"); return null; } // Decode payload (second part) const payload = Buffer.from(parts[1], "base64").toString("utf-8"); const decodedPayload = JSON.parse(payload); // Check if exp claim exists if (typeof decodedPayload.exp !== "number") { console.warn("JWT does not contain expiration time (exp claim)"); return null; } // Convert Unix timestamp (seconds) to Date return new Date(decodedPayload.exp * 1000); } catch (error) { console.warn("Error parsing JWT token:", error); return null; } } /** * Gets a valid token, either from cache or by requesting a new one * @returns Promise resolving to a valid token string */ public async getToken(): Promise { if (this.token && !this.isTokenExpired()) { return this.token; } const config = CoreConfiguration.inst(); const serviceToken = config.mmApiAuthToken; const tokenResponse = await this.api.request({ url: "/user-token", method: "GET", headers: { Authorization: `Bearer ${serviceToken}`, }, }); this.token = tokenResponse.data; this.tokenExpiration = this.getTokenExpiration(this.token); // Cache the new token and its expiration setCachedMMToken({ token: this.token, expiration: this.tokenExpiration?.getTime() || null, }); logger.info("getToken: the api token from MM has been refreshed"); return this.token; } /** * Invalidates the current token, forcing a fresh fetch on next getToken() call */ public invalidateToken(): void { this.token = ""; this.tokenExpiration = null; setCachedMMToken({ token: "", expiration: null }); } /** Release HTTP connections (token fetch client). */ async destroy(): Promise { await this.api.destroy(); } }