Files
kme_content_adapter/src/proxyScripts/kmeContentSourceAdapter.js

101 lines
3.8 KiB
JavaScript

(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);
}
})()