(async () => { try { // 1. Validate required kme_CSA_settings fields const requiredFields = ['tokenUrl', 'username', 'password', 'clientId', 'scope']; for (const field of requiredFields) { if (!kme_CSA_settings[field]) { throw new Error('missing required field: ' + field); } } const { tokenUrl, username, clientId, scope } = kme_CSA_settings; // 2. Read token cache from Redis console.debug({ message: 'Checking token cache', url: req.url, method: req.method }); const token = await redis.hGet('authorization', 'token'); const expiry = parseFloat(await redis.hGet('authorization', 'expiry') ?? '0'); const isValid = token !== null && Date.now() / 1000 < expiry; // 3. Cache HIT → respond immediately if (isValid) { console.debug({ message: 'Token cache hit', expiresIn: Math.round(expiry - Date.now() / 1000) + 's' }); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Authorized'); return; } // 4. Stampede guard — if a fetch is already in flight, queue on it if (kme_CSA_settings._pendingFetch && typeof kme_CSA_settings._pendingFetch.then === 'function') { console.debug({ message: 'Token fetch in flight, queuing request' }); await kme_CSA_settings._pendingFetch; console.debug({ message: 'Queued request unblocked, responding' }); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Authorized'); return; } // 5. Cache MISS → fetch fresh token console.info({ message: 'Token cache miss, fetching fresh token', tokenUrl }); const params = new URLSearchParams({ grant_type: 'password', username, password: kme_CSA_settings.password, client_id: clientId, scope, }); // Set up stampede guard before fetching let resolvePending; let rejectPending; kme_CSA_settings._pendingFetch = new Promise((resolve, reject) => { resolvePending = resolve; rejectPending = reject; }); // Prevent an unhandled-rejection when no concurrent request is waiting on this promise kme_CSA_settings._pendingFetch.catch(() => {}); try { console.debug({ message: 'Requesting new token', url: tokenUrl, method: 'POST' }); const response = await axios.post(tokenUrl, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 5000, }); const { id_token, expires_in } = response.data; if (!id_token) throw new Error('id_token missing from response'); if (!expires_in) throw new Error('expires_in missing from response'); // 6. Write to Redis cache await redis.hSet('authorization', 'token', id_token); await redis.hSet('authorization', 'expiry', String(expires_in)); console.info({ message: 'Token fetched and cached', expiresAt: new Date(expires_in * 1000).toISOString() }); // Resolve the pending fetch promise so waiting requests can proceed resolvePending(); } catch (fetchErr) { console.error({ message: 'Token fetch failed', error: fetchErr.message, code: fetchErr.code }); rejectPending(fetchErr); throw fetchErr; } finally { kme_CSA_settings._pendingFetch = null; } // 7. Respond success res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Authorized'); } catch (err) { let message; if (err.response) { message = 'HTTP ' + err.response.status; } else if (err.code === 'ECONNABORTED' || err.code === 'ERR_CANCELED') { message = 'token service timeout'; } else { message = err.message; } console.error({ message: 'Auth failed', error: message, url: req.url }); res.writeHead(401, { 'Content-Type': 'text/plain' }); res.end('Unauthorized: ' + message); } })()