cmd/librenotes/web/public/auth-client.js exposes window.authClient
with the full session API used by the rest of the frontend:
Session storage (#14):
- saveSession / loadSession / clearSession / isAuthenticated
- Backed by sessionStorage, not localStorage: tokens are isolated
per tab and cleared on tab close. localStorage would survive
tab close on a shared device, which we want to avoid.
- loadSession returns null when expires_at has passed, so callers
treat expired sessions as logged-out without a network round
trip.
API wrapper (#14):
- apiFetch(url, init) attaches Authorization: Bearer <jwt> to
every call. On 401 it clears the session and redirects to
/login.html?next=<current-path> so the user returns where they
started. Throws after the redirect so the caller's .then does
not run with stale data.
Tenant-scoped localStorage (#15):
- tenantStore() returns a get/set/remove wrapper whose keys are
prefixed "librenotes:{user_id}:". Two users on the same browser
therefore have fully independent UI state. JSON serialisation
with try/catch fallbacks for corrupted or quota-exceeded
storage so a bad blob never crashes the app.
- clearTenantStore(userID) removes every key with that prefix.
Called from clearSession() so logout wipes both the JWT and
the user's preferences.
verify.html + verify.js complete the magic-link flow: read
?token=, POST /auth/verify, hand the response to saveSession(),
strip the token from the URL via history.replaceState. Errors
route the user back to /login.html.
app.html + app.js are a minimal authenticated landing demonstrating
the full stack end-to-end: apiFetch hits /api/whoami, tenantStore
persists a theme preference, logout clears both. The full notes
UI is left to a later phase — this is the seam.
Closes#14 and #15.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>