Skip to content

T 088


uid: T-088 title: Auth β€” degraded-link tolerance for the periodic RBAC profile re-read (NAS re-login fix) status: done area: auth created: 2026-06-22 updated: 2026-07-01 related: I-005


RBAC sequence: independent β€” an auth-resilience fix (degraded-link tolerance for the periodic RBAC profile re-read), orthogonal to the enforcement + people tracks. No precede/succeed; parallelisable. See the sequence map in T-022. (Marked by EOP Local Assistance, 2026-06-23.)

Why (owner, 2026-06-22, via I-005)

"Ever since I started hosting the web app on my NAS, I constantly need to re-login to my web app because of reason unknown… It wasn't like this when the web app was hosted on Vercel"

I-005 is the symptom record + full diagnosis. This task is the structural fix.

Root cause (from I-005)

The NextAuth JWT callback re-reads aote-system/users/{uid} every 60s and fails closed (β†’ profileMissing β†’ /auth/error?reason=profile-missing) whenever that read throws. On Vercel (datacenter-adjacent to Firestore) the read never failed; on the NAS (4G/CGNAT uplink) it times out intermittently, so the user is bounced roughly every minute.

The fix + the owner's key constraint

Hold last-known-good RBAC for a bounded grace window when the read fails β€” but only on a genuine degraded-link signal, so it is self-disabling on healthy infrastructure. Owner, 2026-06-22 (verbatim):

"Could we actually have the resilient refresh kicks in if a certain indication of something drops below a certain speed? I don't want to have the web app moving back to Vercel or a professional hosting service one day and have this configuration forgotten and having it cause me safety issues or anything"

Design: the "indication of dropping below a certain speed" = the read failing with a transport/timeout error (the link too slow to complete the read within the deadline). The decision is a pure, unit-tested function shouldHoldLastKnownGood() in lib/auth/degradedLinkGrace.ts:

  • Throw is a transport/timeout error (gRPC 14/4, Node socket errnos, or the equivalent REST message shapes) AND there's a successful read within the last PROFILE_GRACE_MS (10 min) AND the session isn't already profileMissing β†’ HOLD last-known-good RBAC (don't downgrade); retry on the very next request (don't advance the refresh timer).
  • Definitive null (profile genuinely deleted) β†’ fail closed immediately (unchanged).
  • Non-transport error, grace exhausted, or no prior good read β†’ fail closed exactly as before.

Why this satisfies the constraint (no forgotten config): there is no env flag or "lenient mode." On a fast datacenter link the read never raises these transport errors, so shouldHoldLastKnownGood() always returns false and behavior is byte-identical to the original strict fail-closed. The tolerance re-enables itself only when (and exactly as long as) the link is actually timing out. If the app moves back to Vercel/managed hosting, the lenient path simply goes dormant on its own.

Bounded exposure (security): a suspended/deleted user could retain access for at most PROFILE_GRACE_MS (10 min) and only while Firestore is actually unreachable. The session already trusts cached RBAC for up to 60s between refreshes and is capped at the 30-day maxAge; the next successful read re-enforces suspension/expiry. A definitive null (the deletion case) is never held.

Files

  • lib/auth/degradedLinkGrace.ts β€” new. Pure, dependency-free: isTransientLinkError(), shouldHoldLastKnownGood(), PROFILE_GRACE_MS, SLOW_PROFILE_READ_WARN_MS. Isolated so the security decision is auditable + tested outside the 500-line auth route.
  • pages/api/auth/[...nextauth].ts β€” JWT callback: time the read + log a breadcrumb when a successful read is slow (> SLOW_PROFILE_READ_WARN_MS); stamp token._lastGoodProfileAt on success + at sign-in; route the catch through shouldHoldLastKnownGood().
  • __tests__/lib/auth/degradedLinkGrace.test.ts β€” new. 9 cases: transient-error recognition, non-transport/definitive errors fail closed, grace-window expiry, no-prior-good-read, and already-missing not rescued.

Out of scope (candidate follow-ups, not done here)

  • preferRest: true in lib/firebaseAdmin.ts β€” a Vercel cold-start tuning now on a long-lived NAS process. Could be revisited (gRPC keepalive may suit a long-lived process better), but the grace window addresses the user-facing symptom transport-agnostically. Left as a separate question.
  • Restoring DSM creds so a future session can confirm the fix against live NAS logs.

Decision log

2026-06-22 β€” opened I-005 + T-088; implemented the gated-resilient refresh

  • βœ… Attestation (Accounting (Diagnostics)): read AGENTS.md (nightly tip 0d04a26a); scope-scanned board + tickets β€” no existing task covers the NAS re-login; I-002 is a different (client-side) auth issue.
  • Source: Accounting (Diagnostics) Β· https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
  • Proposed by: the owner (reported the symptom; refined the approach to require a degraded-link signal so it's self-disabling). Approved by: the owner β€” chose "resilient refresh," then added the self-disabling constraint (verbatim above).
  • What changed: implemented the fix + the pure decision module + unit tests. tsc clean; full suite 356/356 (41 files, +9 new).
  • Model note: this session's model was switched to Opus 4.8 mid-conversation; identity stays Accounting (Diagnostics) (the session URL is the stable ledger key, per docs/agents-registry.md).
  • UID note: allocated T-088 + ticket I-005 past every in-flight PR β€” #775 holds T-085/T-086, #779 holds T-087, #777/#779 hold I-003/I-004 β€” to stay collision-free in any merge order. See the chronic-collision flag raised to the owner this session (the README "next-free" pointer can't coordinate parallel branches).

2026-07-01 β€” flipped doing β†’ done (close-out)

Status had been left at doing since the 2026-06-22 implementation; flipping now after verification (flagged by the EOP Local Assistance fork, relayed by the owner).

  • Verdict β€” SHIPPED. The gated-resilient refresh (degraded-link tolerance for the periodic RBAC profile re-read) is merged to main and live in production. Verified this session: lib/auth/degradedLinkGrace.ts, the [...nextauth].ts JWT-callback wiring, and the 9-case unit test all exist on main; shipped tsc-clean with the suite green (356/356 per the 2026-06-22 entry). Nothing in scope remains.
  • Commit: 00ddf114 (merge of PR #782, "fix(T-088): degraded-link tolerance for the RBAC profile re-read (closes I-005, NAS re-login)").
  • Blast radius (auth): touches the NextAuth JWT callback / periodic RBAC profile re-read (pages/api/auth/[...nextauth].ts) + the new lib/auth/degradedLinkGrace.ts. Behavior is byte-identical on a healthy link (self-disabling); the only change is on transport/timeout errors, where last-known-good RBAC is held for ≀ PROFILE_GRACE_MS (10 min). Any agent working auth/RBAC enforcement or session refresh should note the fail-closed path now routes through shouldHoldLastKnownGood().
  • Still open (non-blocking follow-ups, unchanged from "Out of scope"): (1) revisit preferRest/gRPC-keepalive tuning in lib/firebaseAdmin.ts for the long-lived NAS process; (2) live-NAS-log confirmation (DSM creds were unavailable) β€” the real-world check is owner-observable (has the ~1-minute re-login stopped?). Neither blocks done; the structural fix is complete and unit-verified.
  • Source: Accounting (Diagnostics) Β· https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea