Skip to content

feat(fides-auth-next): add useHeartbeat hook and handleHeartbeat handler#1473

Merged
losolio merged 1 commit into
mainfrom
feat/auth-heartbeat-lib
May 21, 2026
Merged

feat(fides-auth-next): add useHeartbeat hook and handleHeartbeat handler#1473
losolio merged 1 commit into
mainfrom
feat/auth-heartbeat-lib

Conversation

@losolio
Copy link
Copy Markdown
Contributor

@losolio losolio commented May 21, 2026

Summary

Activity-driven session keepalive on top of the createActivityTracker primitive landed in #1470.

  • useHeartbeat — React hook that ticks every 5 min, gated on user activity within the last 2 min. Pauses on hidden tabs, beats immediately on tab refocus after a long away, and emits onSessionExpired when the refresh endpoint returns 401.
  • handleHeartbeat — Next.js route-handler factory that runs refreshCurrentSession and returns 401 when the refresh token is gone. Rate-limited via the existing globalPOSTRateLimit.
  • New subpath exports: @eventuras/fides-auth-next/store (adds useHeartbeat) and @eventuras/fides-auth-next/heartbeat-handler.

Part 2/3 of the session heartbeat work:

No app behavior change in this PR — these are unused exports until wired up in PR #3.

Motivation

Auth0 idle refresh-token lifetime kicks in even for active users who don't navigate — e.g. long form editing. This is the bridge between "user is interacting" (PR #1) and "tokens get refreshed" (PR #3).

Design notes

  • 5 min interval / 2 min idle threshold chosen so a user typing continuously never lets the access token (15 min default) get within close range of the Auth0 idle refresh window (typically 30+ min)
  • Tab visibility check prevents background tabs from indefinitely extending sessions
  • Callbacks are kept in a ref so re-renders don't restart the timer
  • No unit tests in this PR — libs/fides-auth-next has no vitest setup yet; planned as a follow-up (logged separately)

Test plan

  • pnpm --filter @eventuras/fides-auth-next build emits dist/heartbeat-handler.{js,d.ts} and dist/store/use-heartbeat.{js,d.ts}
  • Changeset present (.changeset/fides-auth-next-heartbeat.md)
  • Type-check clean

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 21, 2026 20:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the missing “heartbeat” layer to @eventuras/fides-auth-next, enabling activity-gated session refresh via a client hook and a Next.js route handler (to be wired into apps/web in the follow-up PR).

Changes:

  • Added useHeartbeat client hook that periodically POSTs a refresh endpoint only when the user has been recently active and the tab is visible.
  • Added handleHeartbeat Next.js route-handler helper that refreshes the current session and returns 401 when refresh is no longer possible.
  • Updated library build/exports and added a changeset for a minor release.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
libs/fides-auth-next/vite.config.ts Adds heartbeat-handler as a build entrypoint.
libs/fides-auth-next/src/store/use-heartbeat.ts New client hook implementing activity-driven keepalive logic.
libs/fides-auth-next/src/store/index.ts Re-exports useHeartbeat and HeartbeatConfig from the store subpath.
libs/fides-auth-next/src/index.ts Re-exports the heartbeat handler from the package root.
libs/fides-auth-next/src/heartbeat-handler.ts New server-side helper to rate-limit and refresh the current session on POST.
libs/fides-auth-next/package.json Adds ./heartbeat-handler export map entry.
.changeset/fides-auth-next-heartbeat.md Declares a minor version bump for the new hook + handler.

Comment on lines +134 to +156
const response = await fetch(endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: { 'content-type': 'application/json' },
});

if (response.status === 401) {
logger.info('Heartbeat returned 401 — session expired');
callbacksRef.current.onSessionExpired?.();
stopped = true;
return;
}

if (!response.ok) {
throw new Error(`Heartbeat failed with status ${response.status}`);
}

logger.debug('Heartbeat refresh succeeded');
callbacksRef.current.onRefreshed?.();
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
logger.warn({ error: err.message }, 'Heartbeat tick failed');
callbacksRef.current.onError?.(err);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — added an AbortController that's aborted in the teardown, plus a stopped re-check after the await fetch(...) so callbacks can't fire after unmount or after a 401. Extracted the cleanup into a teardown() helper that the 401 branch now calls eagerly, so the visibility listener and tracker don't linger after the session is known dead. The catch block also short-circuits on stopped so an aborted fetch doesn't surface as an onError.

Comment on lines +172 to +177
// Tab regained focus. If we've been away longer than one tick, beat now.
const sinceLast = Date.now() - lastTickAt;
if (sinceLast >= intervalMs) {
logger.debug({ sinceLast }, 'Tab regained focus after long away, beating now');
void sendBeat();
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — onVisibilityChange now clears the pending timeoutId before triggering the immediate beat, and chains scheduleNext after the beat resolves so the next tick is measured from now rather than firing back-to-back.

config: HeartbeatHandlerConfig,
): Promise<Response> {
if (request.method !== 'POST') {
return new Response(null, { status: 405, headers: { allow: 'POST' } });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — using Allow (conventional title-case) on the 405.

Comment on lines +73 to +75
return Response.json({
accessTokenExpiresAt: updated.tokens?.accessTokenExpiresAt ?? null,
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good defense-in-depth — added Cache-Control: private, no-store on the 200 success response. (401s aren't cacheable by default per RFC 7234, so leaving those as-is.)

In fides-auth-next: activity-driven session keepalive on top of
@eventuras/fides-auth's createActivityTracker. Refreshes access tokens while
the user is actually interacting, so long form-editing sessions don't hit
the idle refresh-token cut-off.

- useHeartbeat: React hook that ticks every 5 min, gated on activity within
  the last 2 min, pauses in hidden tabs, beats immediately on tab refocus
- handleHeartbeat: Next route-handler factory that runs server-side refresh
  and returns 401 when the refresh token is gone

Part 2/3 of the heartbeat work. PR #3 wires this into apps/web.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@losolio losolio force-pushed the feat/auth-heartbeat-lib branch from 2e8666f to d9121c3 Compare May 21, 2026 21:04
@sonarqubecloud
Copy link
Copy Markdown

@losolio losolio merged commit e275a40 into main May 21, 2026
6 checks passed
@losolio losolio deleted the feat/auth-heartbeat-lib branch May 21, 2026 21:08
@github-project-automation github-project-automation Bot moved this from 🆕 New to ✅ Done in Eventuras backlog May 21, 2026
losolio added a commit that referenced this pull request May 23, 2026
Wires the heartbeat from @eventuras/fides-auth-next into apps/web:

- New `/api/auth/heartbeat` POST route that delegates to the
  `handleHeartbeat` factory with the app's `oauthConfig`
- `useHeartbeat` hook invoked from the root `Providers` so every
  authenticated user benefits — dispatches `sessionExpired` to the
  auth store on 401

Final 3/3 of the heartbeat work. Together with #1470 and #1473, this
should stop the "logged out while typing" reports for active users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
losolio added a commit that referenced this pull request May 24, 2026
Wires the heartbeat from @eventuras/fides-auth-next into apps/web:

- New `/api/auth/heartbeat` POST route that delegates to the
  `handleHeartbeat` factory with the app's `oauthConfig`
- `useHeartbeat` hook invoked from the root `Providers` so every
  authenticated user benefits — dispatches `sessionExpired` to the
  auth store on 401

Final 3/3 of the heartbeat work. Together with #1470 and #1473, this
should stop the "logged out while typing" reports for active users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

2 participants