Skip to content

chore(web): drive server-auth-actions React Doctor cluster to zero (#243) - #499

Merged
thomasluizon merged 2 commits into
mainfrom
chore/rd-server-auth-actions
Jul 13, 2026
Merged

chore(web): drive server-auth-actions React Doctor cluster to zero (#243)#499
thomasluizon merged 2 commits into
mainfrom
chore/rd-server-auth-actions

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

What

Drives the server-auth-actions React Doctor cluster (22 errors — the single largest error cluster) to zero. All findings live in apps/web/app/actions/**: social.ts (8), accountability.ts (5), chat.ts (5), challenges.ts (4).

server-auth-actions is a security rule: it flags any exported Next.js Server Action it cannot prove authenticates its caller (Server Actions are public POST endpoints). It only recognizes an auth call within the first 10 top-level statements of the action.

Verdict: all 22 are false positives

Every flagged action delegates its privileged request to serverAuthFetch (apps/web/lib/server-fetch.ts), which:

  1. calls resolveServerSession() (reads the httpOnly auth_token cookie), and
  2. throws 401 Unauthorized before issuing any request when no session token can be resolved,
  3. then forwards the token as Bearer to the .NET API, which independently authorizes it ([Authorize] default).

React Doctor cannot see this because the serverAuthFetch call is nested inside each file's shared error-envelope closure (runSocialAction / runAccountabilityAction / wrapServerAction / runChallengeAction) rather than being a top-level statement. The directly-calling actions in the same tree (e.g. app/actions/habits.ts) are not flagged, which confirms the closure indirection is the sole cause.

Fix: a justified per-action react-doctor-disable-next-line server-auth-actions suppression (each links #243). Adding an explicit session read at the top of each action would double-guard (a redundant cookie read/refresh) against the codebase's existing pattern, so suppression is the correct call — no new auth mechanism, no masked hole.

Per-action guard-vs-suppress table

Every one of the 22 is a proven false-positive suppression; enforcing path for all: serverAuthFetchresolveServerSession (401 before any fetch) → .NET [Authorize].

File Actions suppressed Enforcing wrapper closure
app/actions/social.ts sendFriendRequest, acceptFriendRequest, removeFriend, sendCheer, blockUser, reportUser, setHandle, setSocialOptIn (8) runSocialAction
app/actions/accountability.ts inviteAccountabilityBuddy, acceptAccountabilityPair, endAccountabilityPair, setAccountabilityHabits, checkInAccountability (5) runAccountabilityAction
app/actions/chat.ts confirmPendingOperation, issuePendingOperationStepUp, verifyPendingOperationStepUp, executePendingOperation, resolveClarification (5) wrapServerAction
app/actions/challenges.ts createChallenge, joinChallenge, leaveChallenge, setChallengeHabits (4) runChallengeAction

No genuine guard-adds were needed — auth was already enforced upstream for all 22.

Tests

Added unauthenticated-rejection coverage proving each action family surfaces the serverAuthFetch 401 as { ok: false, status: 401 } rather than executing:

  • new __tests__/actions/social.test.ts, challenges.test.ts, accountability.test.ts
  • extended __tests__/actions/chat.test.ts

(Backed by the existing __tests__/lib/server-fetch.test.ts case: serverAuthFetch throws 401 and never calls fetch when no session token resolves.)

Verification

  • React Doctor full apps/web scan: server-auth-actions = 0, app/actions/** = 0 findings (error count 54 → 32).
  • React Doctor --scope changed --base origin/main --blocking error: 0 new errors.
  • apps/web lint: 0 errors · type-check: clean · vitest: 2259 passed / 274 files.

Parity

Web-only change: mobile talks to the API directly (apiClient carries the SecureStore Bearer) and has no Server Actions, so there is no mobile mirror for this rule.

Refs #243 (React Doctor burn-down: server-auth-actions)

)

React Doctor's `server-auth-actions` rule flagged 22 exported Server Actions
across app/actions/{social,accountability,chat,challenges}.ts. All 22 are false
positives: each action delegates the privileged request to `serverAuthFetch`
(lib/server-fetch.ts), which resolves the session cookie via
`resolveServerSession` and throws a 401 before any fetch when no session is
present; the .NET API then authorizes the forwarded Bearer token. The rule only
recognizes an auth call in the first 10 top-level statements of the action, and
here the `serverAuthFetch` call is nested inside the shared error-envelope
closure (runSocialAction / runAccountabilityAction / wrapServerAction /
runChallengeAction), which it cannot trace.

Applied a justified per-action `react-doctor-disable-next-line
server-auth-actions` suppression (each linking #243) rather than double-guarding
with a redundant session read. app/actions/** now reports 0 React Doctor
findings. Added unauthenticated-rejection tests proving each action family
surfaces the serverAuthFetch 401 as `{ ok: false, status: 401 }` instead of
executing.

Refs #243 (React Doctor burn-down: server-auth-actions)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jul 13, 2026 9:20pm

Request Review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review: PR #499chore(web): drive server-auth-actions React Doctor cluster to zero (#243)

Recommendation: APPROVE

Severity Count
Critical 0
High 0
Medium 0
Low / Info 1

What the diff does

Adds 22 justified react-doctor-disable-next-line server-auth-actions suppression comments (each linking issue #243) across apps/web/app/actions/{social,accountability,chat,challenges}.ts, plus unauthenticated-rejection unit tests proving each suppression is safe. No production logic changed — comments and tests only.

Verification performed

  • Read apps/web/lib/server-fetch.ts:29-45: confirmed serverAuthFetch calls resolveServerSession() and throws createApiClientError(401, …) before any fetch when session.token is missing — the PR's false-positive claim is accurate, not just asserted.
  • Read all four action files in full: exactly 22 exported functions got the suppression comment (8 social + 5 accountability + 5 chat + 4 challenges).
  • Confirmed the WHY comments satisfy eslint-rules/no-comments.cjs's URL allowance (real github.com/.../issues/243 link) — legitimate under local/no-comments, not an accidental pass.
  • New test files follow the established vi.mock('@/lib/server-fetch', …) + dynamic-import pattern already used in the repo.
  • Parity dimension, investigated in depth (two independent checks disagreed, resolved by direct verification): gh pr diff 499 --name-only confirms only 8 web files changed, no apps/mobile/** files touched. Grepped apps/mobile for 'use server' — zero matches, confirming the React Doctor server-auth-actions cluster is a Next.js Server Actions concept that structurally doesn't exist on mobile (mobile mirror hooks route through apiClient with a SecureStore Bearer token instead). Mobile already has centralized 401-handling coverage at apps/mobile/__tests__/lib/api-client.test.ts and query-client.test.ts — the mirror-layer equivalent of web's pre-existing __tests__/lib/server-fetch.test.ts. The new per-action-file tests in this PR exist specifically to satisfy the React Doctor lint tool's suppression-justification requirement, which has no mobile analog. This is a platform-adapter-only difference (permitted under root CLAUDE.md), not an unaddressed parity gap — no mobile changes required.
  • Live CI (gh pr view --json statusCheckRollup): React Doctor = SUCCESS (the exact gate this PR targets), plus Lint, Type Check, Contract Drift, Design Token Guard, CodeQL, GitGuardian all SUCCESS. Unit Tests / SonarCloud / this review were still IN_PROGRESS at review time.
  • No prior reviews or unresolved threads on this PR to reconcile against.

Findings

No Critical/High/Medium findings. One Info-level note: the per-file 401 tests cover a representative subset of each file's actions rather than all 22 individually, but every action funnels through the already-tested serverAuthFetch path, so this isn't a real gap.

Subagents

  • parity-checker: consulted twice with conflicting verdicts (N/A vs PARTIAL); resolved directly above via diff scope + grep + existing mobile test-coverage inspection — no mobile changes required, verdict is N/A.
  • i18n-syncer, contract-aligner, security-reviewer, design-reviewer: N/A — their gates (user-facing strings, shared/DTO changes, orbit-api changes, UI files) aren't touched by this diff.

Deferred

Could not execute npx react-doctor@0.7.6 locally (no outbound network access in this sandbox) — deferred to the PR's own live "React Doctor" required CI check, which reports SUCCESS.

No code changes requested. Safe to merge once remaining in-progress required checks resolve green.

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 58845ff into main Jul 13, 2026
20 checks passed
@thomasluizon
thomasluizon deleted the chore/rd-server-auth-actions branch July 13, 2026 21:28

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

/pr-review — PR #499

Scope: chore(web): drive server-auth-actions React Doctor cluster to zero (#243)
Recommendation: APPROVE

Summary

Web-only, low-risk change: adds one react-doctor-disable-next-line server-auth-actions WHY comment above each of 22 exported Server Actions across apps/web/app/actions/{social,accountability,chat,challenges}.ts, plus new/extended Vitest coverage. No runtime logic changed.

Independently traced serverAuthFetch (apps/web/lib/server-fetch.ts) and confirmed it throws a 401 ApiClientError before any fetch() call when resolveServerSession() returns no token, backed by the existing test apps/web/__tests__/lib/server-fetch.test.ts. The suppression's factual premise is sound, and choosing suppression over a redundant top-level auth re-check is the correct call under root CLAUDE.md rule 1 (root-cause over workarounds).

Severity Count
Critical (incl. ⚠️ old-client breaks) 0
High 0
Medium 1
Low / Info 0

Findings

[Medium] Three of the 22 newly-suppressed actions ship with zero test coverage

  • location: apps/web/app/actions/social.ts (blockUser, setSocialOptIn), apps/web/app/actions/chat.ts (resolveClarification)
  • issue: The PR body claims coverage was added "proving each action family surfaces the serverAuthFetch 401." Verified directly: apps/web/__tests__/actions/social.test.ts only imports/exercises sendFriendRequest, acceptFriendRequest, removeFriend, sendCheer, reportUser, setHandleblockUser and setSocialOptIn are never imported or tested. Likewise resolveClarification (which has its own untested local validation: a UUID regex check and a clarification-length bound) has no test reference anywhere in apps/web/__tests__/actions/chat.test.ts.
  • risk: Low today (the shared enforcement path is covered indirectly via sibling actions in the same closure), but a future refactor of the closure wrappers or of resolveClarification's validation branch could silently regress with nothing to catch it.
  • fix: Add the same 401-rejection assertion pattern used for the other 19 actions in this PR to these three, plus a minimal happy-path test for resolveClarification's validation branches.

Subagents

Agent Verdict
parity-checker PAIRED — confirmed apps/mobile has no Server Actions concept at all (no apps/mobile/app/actions/*, no 'use server'); the suppressed lint rule and its tests are a web-only platform-adapter surface, correctly out of scope for a mobile mirror.
i18n-syncer N/A — no user-facing strings changed
contract-aligner N/A — no packages/shared/src/types/*/endpoints.ts change, orbit-api untouched
security-reviewer N/A — orbit-api untouched, not checked out in this job
design-reviewer N/A — no UI/visual file changed (Server Action logic + tests only)

Deferred

  • Dimensions requiring UI/i18n/contract/backend changes: N/A per gate (no such changes in this diff).
  • Cross-repo (orbit-api) dimensions: not verifiable — orbit-api is not checked out in this CI job.
  • Phase 6 (/validate): skipped per workflow instruction — Build / Unit Tests / SonarCloud run as separate required checks on this PR. PR body reports 0 lint errors, clean typecheck, 2259/2259 tests passing.
  • Whether react-doctor@0.7.6 recognizes the react-doctor-disable-next-line directive as a real suppression vs. inert prose satisfying local/no-comments's WHY+URL exception: not independently reproducible in this job. The PR body's own verification numbers (server-auth-actions = 0, 0 new errors under --scope changed --blocking error) are the only evidence — recommend confirming the React Doctor CI check is green before merge.

What's good

  • The false-positive analysis is genuinely verified, not just asserted — traced end to end with an existing test backing the core claim.
  • Correctly declines to add a redundant auth guard (would violate root CLAUDE.md rule 1) and correctly scopes as web-only with independently-confirmed reasoning (mobile has no Server Actions).
  • New tests exercise real production logic (the error-envelope translation in the closures), not tautological mock-echoes.
  • All 22 suppression comments are placed correctly, worded consistently, and pass the lint rule's WHY+URL exception.

thomasluizon added a commit that referenced this pull request Jul 13, 2026
…501)

* chore(web): drive React Doctor to zero across web app routes (#243)

Burn down all React Doctor findings under apps/web/app/** (excluding the
actions/ server-auth cluster cleared in #499) — 91 findings to zero.

Fixed properly:
- no-impure-state-updater: hoist the nested setState out of the
  useTodaySearch toggle updater (+ unit test).
- no-unguarded-browser-global: read the show-general preference via an
  SSR-safe useSyncExternalStore instead of during render.
- nextjs-no-use-search-params-without-suspense (6): wrap each
  useSearchParams consumer in a <Suspense> boundary.
- no-inline-exhaustive-style (14): hoist static style objects to module
  scope (or spread a static base for the one dynamic case).
- js-set-map-lookups / js-combine-iterations / js-hoist-intl /
  prefer-module-scope-static-value / button-has-type / only-export-components /
  prefer-use-effect-event / label-has-associated-control: mechanical fixes.

Justified react-doctor-disable-next-line suppressions (WHY + #243):
- exhaustive-deps (11): values derived from profile/query data are
  recomputed every render and already listed; the rule unwraps them to the
  source member expression (false positive).
- no-prop-callback-in-render (3): documented adjusting-state-during-render
  sync of idempotent store setters.
- query-mutation-missing-invalidation (4): optimistic patchProfile
  (setQueryData) + rollback keeps the cache in sync.
- nextjs-no-client-side-redirect (4): gates depend on client-fetched
  profile / matchMedia, not resolvable server-side.
- use-lazy-motion (5): LazyMotion migration is app-wide and cannot be
  partially applied per file.
- no-tiny-text (10): intentional captions/badges/eyebrows per DESIGN.md.
- no-many-boolean-props (5) / no-giant-component (2) / prefer-useReducer (1):
  private single-use surfaces; refactor deferred without visual QA.
- url-prefilled-privileged-action / no-fetch-in-effect / prefer-html-dialog /
  no-locale-format-in-render / no-array-index-as-key (3) / no-outline-none:
  the rule's required mitigation is already present or inapplicable.

Refs #243 (React Doctor burn-down: web app routes)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(web): fix no-floating-promises in useTodaySearch test

Use a block-body act() callback so it resolves to the void overload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (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

None yet

Development

Successfully merging this pull request may close these issues.

1 participant