Overview
Authentication tokens are minted and verified in src/lib/auth/jwt.ts, attached to REST calls ad-hoc by src/lib/apiInterceptors.ts, and then read independently and directly from storage by the socket layers (src/lib/websocketManager.ts, src/lib/notifications/socket.ts) and the offline replay queue (src/services/offlineSync.ts, src/services/offlineApi.ts). There is no single source of truth for access-token expiry and no coordinated refresh. As a result: an expired token silently fails live sockets and offline replays, concurrent 401s trigger multiple parallel refreshes (thundering herd), and a rotated or revoked token is never propagated to already-open socket connections. This is both a reliability and a security problem — a revoked session can continue to receive realtime data until the socket happens to drop.
This issue introduces a single token lifecycle owner and wires every consumer (REST, WebSocket, GraphQL, offline queue) through it, with single-flight silent refresh, rotation/revocation propagation, and auth-lifecycle metrics. It is a deliberately cross-cutting change spanning ~15 files and ~900 lines including tests.
Specifications
Features:
Introduce a single TokenManager (src/lib/auth/tokenManager.ts) that owns the access and refresh tokens, decodes exp, and exposes getValidAccessToken() with a single-flight refresh so concurrent callers share one in-flight refresh promise instead of each issuing their own network call
Proactively schedule a silent refresh a configurable skew before exp, queuing in-flight REST, socket, and offline operations until the refresh resolves, then resuming them transparently
Propagate token rotation to all open WebSocket connections via a re-auth handshake, and propagate revocation by disconnecting sockets and halting the offline replay queue before it drains
Emit auth lifecycle metrics (refresh_success, refresh_failure, token_rotated, forced_logout) through src/lib/monitoring/metrics.ts
Tasks:
Build TokenManager with single-flight refresh, skew-based proactive scheduling, and an internal event bus (token:rotated, token:revoked, auth:logout)
Refactor the apiInterceptors request interceptor to call getValidAccessToken(), and route the response 401 interceptor through TokenManager.refresh(), replaying the original request after a successful refresh
Add a socket auth adapter so websocketManager and notifications/socket re-send their auth payload on token:rotated and disconnect on token:revoked
Gate offlineSync/offlineApi replay on a valid token: refresh before draining the queue, and dead-letter items that fail after a forced logout instead of retrying forever
Surface exp/nbf failure reasons from jwt.ts verification, and add environment.ts config for the refresh skew and refresh endpoint, plus new keys in app.constants.ts
Write unit tests covering concurrent-refresh dedupe, skew scheduling, socket re-auth on rotation, and offline replay being blocked after logout
Impacted Files:
src/lib/auth/tokenManager.ts (new)
src/lib/auth/jwt.ts
src/lib/apiInterceptors.ts
src/lib/api.ts
src/lib/authMiddleware.ts
src/lib/websocketManager.ts
src/lib/notifications/socket.ts
src/services/offlineApi.ts
src/services/offlineSync.ts
src/hooks/useApi.ts
src/hooks/useWebSocket.ts
src/store/stateManager.ts
src/lib/monitoring/metrics.ts
src/config/environment.ts
src/constants/app.constants.ts
src/lib/auth/tests/tokenManager.test.ts (new)
Acceptance Criteria
A single access-token refresh occurs under concurrent 401s — no duplicate refresh network calls are made
Access tokens refresh silently before expiry using the configured skew, and in-flight REST, socket, and offline operations resume without a user-visible failure
Token rotation triggers a socket re-auth handshake; revocation disconnects all sockets and halts offline replay with dead-lettering
Auth lifecycle metrics are emitted with the documented tags, and a forced logout clears tokens from every consumer (REST, sockets, offline queue)
Unit tests cover dedupe, skew scheduling, socket re-auth, and offline gating, with >80% coverage on tokenManager.ts
Overview
Authentication tokens are minted and verified in
src/lib/auth/jwt.ts, attached to REST calls ad-hoc bysrc/lib/apiInterceptors.ts, and then read independently and directly from storage by the socket layers (src/lib/websocketManager.ts,src/lib/notifications/socket.ts) and the offline replay queue (src/services/offlineSync.ts,src/services/offlineApi.ts). There is no single source of truth for access-token expiry and no coordinated refresh. As a result: an expired token silently fails live sockets and offline replays, concurrent 401s trigger multiple parallel refreshes (thundering herd), and a rotated or revoked token is never propagated to already-open socket connections. This is both a reliability and a security problem — a revoked session can continue to receive realtime data until the socket happens to drop.This issue introduces a single token lifecycle owner and wires every consumer (REST, WebSocket, GraphQL, offline queue) through it, with single-flight silent refresh, rotation/revocation propagation, and auth-lifecycle metrics. It is a deliberately cross-cutting change spanning ~15 files and ~900 lines including tests.
Specifications
Features:
Introduce a single
TokenManager(src/lib/auth/tokenManager.ts) that owns the access and refresh tokens, decodesexp, and exposesgetValidAccessToken()with a single-flight refresh so concurrent callers share one in-flight refresh promise instead of each issuing their own network callProactively schedule a silent refresh a configurable skew before
exp, queuing in-flight REST, socket, and offline operations until the refresh resolves, then resuming them transparentlyPropagate token rotation to all open WebSocket connections via a re-auth handshake, and propagate revocation by disconnecting sockets and halting the offline replay queue before it drains
Emit auth lifecycle metrics (
refresh_success,refresh_failure,token_rotated,forced_logout) throughsrc/lib/monitoring/metrics.tsTasks:
Build
TokenManagerwith single-flight refresh, skew-based proactive scheduling, and an internal event bus (token:rotated,token:revoked,auth:logout)Refactor the
apiInterceptorsrequest interceptor to callgetValidAccessToken(), and route the response 401 interceptor throughTokenManager.refresh(), replaying the original request after a successful refreshAdd a socket auth adapter so
websocketManagerandnotifications/socketre-send their auth payload ontoken:rotatedand disconnect ontoken:revokedGate
offlineSync/offlineApireplay on a valid token: refresh before draining the queue, and dead-letter items that fail after a forced logout instead of retrying foreverSurface
exp/nbffailure reasons fromjwt.tsverification, and addenvironment.tsconfig for the refresh skew and refresh endpoint, plus new keys inapp.constants.tsWrite unit tests covering concurrent-refresh dedupe, skew scheduling, socket re-auth on rotation, and offline replay being blocked after logout
Impacted Files:
src/lib/auth/tokenManager.ts (new)
src/lib/auth/jwt.ts
src/lib/apiInterceptors.ts
src/lib/api.ts
src/lib/authMiddleware.ts
src/lib/websocketManager.ts
src/lib/notifications/socket.ts
src/services/offlineApi.ts
src/services/offlineSync.ts
src/hooks/useApi.ts
src/hooks/useWebSocket.ts
src/store/stateManager.ts
src/lib/monitoring/metrics.ts
src/config/environment.ts
src/constants/app.constants.ts
src/lib/auth/tests/tokenManager.test.ts (new)
Acceptance Criteria
A single access-token refresh occurs under concurrent 401s — no duplicate refresh network calls are made
Access tokens refresh silently before expiry using the configured skew, and in-flight REST, socket, and offline operations resume without a user-visible failure
Token rotation triggers a socket re-auth handshake; revocation disconnects all sockets and halts offline replay with dead-lettering
Auth lifecycle metrics are emitted with the documented tags, and a forced logout clears tokens from every consumer (REST, sockets, offline queue)
Unit tests cover dedupe, skew scheduling, socket re-auth, and offline gating, with >80% coverage on
tokenManager.ts