From c1d73b6ba833278712f7d06eccc1a4e51fc6f811 Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Thu, 25 Jun 2026 16:02:44 +0800 Subject: [PATCH] fix(guest): cap guest session TTL on touch The sliding-refresh branch in validateAndTouch hardcoded SESSION_TTL_MS (7d) for all roles, so a guest session created with GUEST_SESSION_TTL_MS (1d) was wrongly bumped to 7d on the first touch after the touch interval. Derive the touch TTL from row.role instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/data/sessions.test.ts | 36 ++++++++++++++++++++++++++++++++++++ src/data/sessions.ts | 5 ++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/data/sessions.test.ts b/src/data/sessions.test.ts index ff13548..13faf14 100644 --- a/src/data/sessions.test.ts +++ b/src/data/sessions.test.ts @@ -160,4 +160,40 @@ describe("guest sessions", () => { const { expiresAt } = sessions.createSession("__guest__", { ttlMs: GUEST_SESSION_TTL_MS, skipCap: true }); expect(expiresAt).toBeLessThanOrEqual(Date.now() + GUEST_SESSION_TTL_MS + 50); }); + + it("validateAndTouch refreshes a guest session to GUEST_SESSION_TTL_MS (1d), not SESSION_TTL_MS (7d)", () => { + const { token } = sessions.createSession("__guest__", { ttlMs: GUEST_SESSION_TTL_MS, skipCap: true }); + // Force the touch branch: backdate lastSeenAt past the touch interval. + botDb.db + .prepare("UPDATE sessions SET lastSeenAt = ? WHERE userId = '__guest__'") + .run(Date.now() - (SESSION_TOUCH_INTERVAL_MS + 1000)); + const result = sessions.validateAndTouch(token); + expect(result?.role).toBe("guest"); + const row = botDb.db + .prepare("SELECT expiresAt FROM sessions WHERE userId = '__guest__'") + .get() as { expiresAt: number }; + // Should refresh to ~now + 1 day, NOT now + 7 days. + expect(row.expiresAt).toBeGreaterThan(Date.now() + GUEST_SESSION_TTL_MS - 5000); + expect(row.expiresAt).toBeLessThanOrEqual(Date.now() + GUEST_SESSION_TTL_MS + 5000); + // Sanity: well below the 7d window. + expect(row.expiresAt).toBeLessThan(Date.now() + SESSION_TTL_MS); + }); + + it("validateAndTouch still refreshes a non-guest (admin) session to SESSION_TTL_MS (7d) on touch", () => { + botDb.db + .prepare("INSERT INTO users (id, username, passwordHash, createdAt, updatedAt, role) VALUES ('admin1','adminuser','!',?,?, 'admin')") + .run(Date.now(), Date.now()); + const { token } = sessions.createSession("admin1"); + botDb.db + .prepare("UPDATE sessions SET lastSeenAt = ? WHERE userId = 'admin1'") + .run(Date.now() - (SESSION_TOUCH_INTERVAL_MS + 1000)); + const result = sessions.validateAndTouch(token); + expect(result?.role).toBe("admin"); + const row = botDb.db + .prepare("SELECT expiresAt FROM sessions WHERE userId = 'admin1'") + .get() as { expiresAt: number }; + // Refreshes to ~now + 7 days, NOT the 1d guest window. + expect(row.expiresAt).toBeGreaterThan(Date.now() + SESSION_TTL_MS - 5000); + expect(row.expiresAt).toBeLessThanOrEqual(Date.now() + SESSION_TTL_MS + 5000); + }); }); diff --git a/src/data/sessions.ts b/src/data/sessions.ts index 6da5930..c9f1fde 100644 --- a/src/data/sessions.ts +++ b/src/data/sessions.ts @@ -83,7 +83,10 @@ export function createSessionStore(db: Database.Database): SessionStore { return null; } if (now - row.lastSeenAt > SESSION_TOUCH_INTERVAL_MS) { - touchStmt.run(now, now + SESSION_TTL_MS, id); + // Refresh against the role's own TTL — guests are short-lived (1d) and + // must NOT be bumped to the member/admin 7d window on touch. + const ttl = row.role === "guest" ? GUEST_SESSION_TTL_MS : SESSION_TTL_MS; + touchStmt.run(now, now + ttl, id); } return { userId: row.userId, username: row.username, role: row.role as "admin" | "member" | "guest" }; },