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 alreadyprofileMissingβ 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); stamptoken._lastGoodProfileAton success + at sign-in; route the catch throughshouldHoldLastKnownGood().__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: trueinlib/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 tip0d04a26a); 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.
tscclean; 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
mainand live in production. Verified this session:lib/auth/degradedLinkGrace.ts, the[...nextauth].tsJWT-callback wiring, and the 9-case unit test all exist onmain; 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 newlib/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 throughshouldHoldLastKnownGood(). - Still open (non-blocking follow-ups, unchanged from "Out of scope"):
(1) revisit
preferRest/gRPC-keepalive tuning inlib/firebaseAdmin.tsfor 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 blocksdone; the structural fix is complete and unit-verified. - Source: Accounting (Diagnostics) Β· https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea