Keepalive via refresh-session; GET pings measured insufficient
The endurance test refuted the sliding-window model I committed earlier. A keepalive doing only GET /api/v3/me succeeded at t+0/30/60/90 and was still rejected by t+120 — consistent with the session ending ~2h after LOGIN (t+107), and inconsistent with 2h after the last request, which would have been t+210. This is a live-vs-source divergence, not a misreading: both the current JwtWhitelistAdapter and the legacy Feathers ensureTokenIsWhitelisted re-set the Valkey TTL on every authenticated request, so the source reads as a sliding window. The instance does not behave that way. So the keepalive now calls POST /authentication/refresh-session, the endpoint behind the UI's "Sitzung verlängern" button, which a separate 100s test showed does hold the reported budget at 7200s. It is the only non-GET request in the server: no body, touches only our own session, cannot read or modify user data, and is not exposed as a tool, so no model-driven call can ever be a POST. It logs the returned budget, which makes a failing extension visible before the session is lost. Whether this is sufficient is NOT established. Two mechanisms still fit: an idle TTL that reads fail to refresh (keepalive works), or an absolute cap/revocation anchored at login — e.g. the IDP's back-channel logout, which clears every token for the account rather than one. Added scripts/session-diagnose.mjs to settle it: it logs the budget every 10 min, so a decaying series indicates the former and an abrupt 401 at 7200s the latter. Docs state the open question rather than asserting a mechanism. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,11 @@ function fakeClient(outcomes: (Error | 'ok')[]) {
|
||||
return {
|
||||
calls,
|
||||
client: {
|
||||
me: async () => {
|
||||
extendSession: async () => {
|
||||
const outcome = outcomes[calls.length] ?? 'ok';
|
||||
calls.push(Date.now());
|
||||
if (outcome !== 'ok') throw outcome;
|
||||
return {} as never;
|
||||
return { expiresInSeconds: 7200 };
|
||||
},
|
||||
} as never,
|
||||
};
|
||||
@@ -41,7 +41,7 @@ describe('SessionKeepalive', () => {
|
||||
});
|
||||
|
||||
it('stops permanently on 401 — a dead session cannot be revived by retrying', async () => {
|
||||
const unauthorized = new SchulcloudApiError(401, '/api/v3/me', '');
|
||||
const unauthorized = new SchulcloudApiError(401, '/api/v3/authentication/refresh-session', '');
|
||||
const { client, calls } = fakeClient([unauthorized]);
|
||||
const messages: string[] = [];
|
||||
const keepalive = new SessionKeepalive(client, 15, 15, (message) => messages.push(message));
|
||||
@@ -75,3 +75,16 @@ describe('SessionKeepalive', () => {
|
||||
assert.equal(calls.length, seen, 'no pings after stop()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SessionKeepalive logging', () => {
|
||||
it('reports the remaining session budget on a successful extension', async () => {
|
||||
const messages: string[] = [];
|
||||
const client = { extendSession: async () => ({ expiresInSeconds: 7200 }) } as never;
|
||||
const keepalive = new SessionKeepalive(client, 60_000, 1000, (m) => messages.push(m));
|
||||
keepalive.start();
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
keepalive.stop();
|
||||
assert.equal(messages.length, 1);
|
||||
assert.match(messages[0]!, /session extended, 7200s \(120 min\)/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user