Found by plugin zero in production: Tideholm live on nostr.social uses the documented programmatic flow — POST /idp/credentials → Bearer token — as its browser login ("sign in with your pod"). The token dies at its fixed 3600s TTL and there is no way to renew it short of re-sending the password: mid-session, every request starts 401ing.
Tideholm mitigated app-side (it mints its own session cookie once getAgent verifies the identity), but that trade is only available to apps with their own session machinery, and it means the app session outlives the pod credential. The gap is JSS's to close: the programmatic flow is the only auth path a vanilla-JS app can realistically speak (full Solid-OIDC + DPoP is a heavy client lift), and it's the only path with no renewal story.
Proposal
POST /idp/refresh — authenticated by a still-valid Bearer token, returns a fresh token for the same WebID:
POST /idp/refresh
Authorization: Bearer <current, unexpired token>
→ { "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "webid": "..." }
- Clients refresh proactively (e.g. at 80% of TTL) and a session lasts as long as the app is actually in use — an idle hour still ends it, preserving the short-TTL security posture.
- Optional hardening: an absolute cap (e.g. refresh chains max out at 24h from the original credential grant), so a stolen token can't be kept alive forever. The original grant time can ride in the token payload.
- Rate-limited like the other
/idp/* endpoints.
Alternatives considered
- Configurable TTL (
createServer({ tokenTtl }) or an expires_in request param): simpler, but "long-lived bearer token in localStorage" is strictly worse than sliding renewal; could still be worth having for agent use-cases.
- Refresh tokens (separate long-lived credential): the OIDC-proper answer, but heavier than this flow's spirit;
/idp/credentials is deliberately the simple path.
Acceptance
Found by plugin zero in production: Tideholm live on nostr.social uses the documented programmatic flow —
POST /idp/credentials→ Bearer token — as its browser login ("sign in with your pod"). The token dies at its fixed 3600s TTL and there is no way to renew it short of re-sending the password: mid-session, every request starts 401ing.Tideholm mitigated app-side (it mints its own session cookie once
getAgentverifies the identity), but that trade is only available to apps with their own session machinery, and it means the app session outlives the pod credential. The gap is JSS's to close: the programmatic flow is the only auth path a vanilla-JS app can realistically speak (full Solid-OIDC + DPoP is a heavy client lift), and it's the only path with no renewal story.Proposal
POST /idp/refresh— authenticated by a still-valid Bearer token, returns a fresh token for the same WebID:/idp/*endpoints.Alternatives considered
createServer({ tokenTtl })or anexpires_inrequest param): simpler, but "long-lived bearer token in localStorage" is strictly worse than sliding renewal; could still be worth having for agent use-cases./idp/credentialsis deliberately the simple path.Acceptance
POST /idp/refreshissues a fresh token from a valid one (401 on expired/invalid)/idp/credentials