[Spec Kit] Implementation progress
This commit is contained in:
100
src/proxyScripts/kmeContentSourceAdapter.js
Normal file
100
src/proxyScripts/kmeContentSourceAdapter.js
Normal file
@@ -0,0 +1,100 @@
|
||||
(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);
|
||||
}
|
||||
})()
|
||||
Reference in New Issue
Block a user