From 2b58068a1a57db163fdfe2a84b16c37233c4550b Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 28 Feb 2026 22:33:31 -0800 Subject: [PATCH] feat: persistent sessions with 30-day JWT and auto-refresh on page load Sessions now last 30 days instead of 15 minutes. Both the rstack-identity component and legacy header auto-refresh the token when < 7 days remain, so users who visit at least once every ~23 days stay logged in indefinitely. Co-Authored-By: Claude Opus 4.6 --- lib/rspace-header.ts | 21 +++++++++++++++++++++ shared/components/rstack-identity.ts | 21 +++++++++++++++++++++ src/encryptid/server.ts | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/rspace-header.ts b/lib/rspace-header.ts index 1df80f0..1d10bb9 100644 --- a/lib/rspace-header.ts +++ b/lib/rspace-header.ts @@ -898,6 +898,27 @@ export function mountHeader(options: HeaderOptions): void { // Mount to DOM document.body.prepend(header); renderHeader(); + + // Auto-refresh token if nearing expiry (< 7 days remaining) + (async () => { + const session = getSession(); + if (!session) return; + const now = Math.floor(Date.now() / 1000); + const remaining = session.claims.exp - now; + if (remaining >= 7 * 24 * 60 * 60) return; + try { + const res = await fetch(`${ENCRYPTID_URL}/api/session/refresh`, { + method: 'POST', + headers: { Authorization: `Bearer ${session.accessToken}` }, + }); + if (!res.ok) return; + const { token } = await res.json(); + if (token) { + storeSession(token, session.claims.username || '', session.claims.did || ''); + renderHeader(); + } + } catch { /* silently ignore */ } + })(); } /** diff --git a/shared/components/rstack-identity.ts b/shared/components/rstack-identity.ts index 467f627..6c51686 100644 --- a/shared/components/rstack-identity.ts +++ b/shared/components/rstack-identity.ts @@ -186,6 +186,7 @@ export class RStackIdentity extends HTMLElement { } connectedCallback() { + this.#refreshIfNeeded(); this.#render(); this.#startNotifPolling(); } @@ -194,6 +195,26 @@ export class RStackIdentity extends HTMLElement { this.#stopNotifPolling(); } + async #refreshIfNeeded() { + const session = getSession(); + if (!session) return; + const now = Math.floor(Date.now() / 1000); + const remaining = session.claims.exp - now; + if (remaining >= 7 * 24 * 60 * 60) return; // more than 7 days left, skip + try { + const res = await fetch(`${ENCRYPTID_URL}/api/session/refresh`, { + method: "POST", + headers: { Authorization: `Bearer ${session.accessToken}` }, + }); + if (!res.ok) return; + const { token } = await res.json(); + if (token) { + storeSession(token, session.claims.username || "", session.claims.did || ""); + this.#render(); + } + } catch { /* silently ignore – user keeps current token */ } + } + #startNotifPolling() { this.#stopNotifPolling(); if (!getSession()) return; diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index e127140..0ea967e 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -90,7 +90,7 @@ const CONFIG = { if (!secret) throw new Error('JWT_SECRET environment variable is required'); return secret; })(), - sessionDuration: 15 * 60, // 15 minutes + sessionDuration: 30 * 24 * 60 * 60, // 30 days refreshDuration: 7 * 24 * 60 * 60, // 7 days smtp: { host: process.env.SMTP_HOST || 'mail.rmail.online',