Fix duplicate /users/me requests during console sign-in - #40
Fix duplicate /users/me requests during console sign-in#40PasinduYeshan wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughChangesProtectedRoute sign-in flow
Session refresh coordination
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant ProtectedRoute
participant ThunderIDRuntime
participant Navigation
participant Logger
ProtectedRoute->>ThunderIDRuntime: read authentication state
ProtectedRoute->>Navigation: navigate to signInUrl
ProtectedRoute->>ThunderIDRuntime: initiate signIn
ThunderIDRuntime-->>ProtectedRoute: return sign-in failure
ProtectedRoute->>Logger: log ThunderIDRuntimeError
sequenceDiagram
participant ThunderIDProvider
participant ThunderIDReactClient
participant SessionUpdate
participant UsersMeAPI
ThunderIDProvider->>ThunderIDReactClient: startAutoRefreshToken
ThunderIDProvider->>ThunderIDReactClient: check isSignedIn
ThunderIDProvider->>SessionUpdate: request updateSession
SessionUpdate->>UsersMeAPI: fetch user profile
ThunderIDProvider->>SessionUpdate: issue concurrent update
SessionUpdate-->>ThunderIDProvider: reuse pending promise
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed: private package registry requires authentication. Disable ESLint in CodeRabbit settings or use public packages. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/react-router/src/components/ProtectedRoute.tsx`:
- Around line 169-228: Update the guard reset logic in the ProtectedRoute
sign-in useEffect so isLoading changes do not clear hasInitiatedSignInRef while
sign-in remains needed; reset it only when isSignedIn, fallback, or redirectTo
indicates sign-in is no longer required. In the signIn rejection handler, clear
hasInitiatedSignInRef before logging the error so a later recovery can retry.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: fff3a076-f714-4b99-8920-6df21515c9ad
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
packages/react-router/.gitignorepackages/react-router/package.jsonpackages/react-router/src/components/ProtectedRoute.tsxpackages/react-router/src/components/__tests__/ProtectedRoute.test.tsxpackages/react-router/tsconfig.spec.jsonpackages/react/src/contexts/ThunderID/ThunderIDProvider.tsxpackages/react/src/contexts/ThunderID/__tests__/ThunderIDProvider.test.tsxpackages/react/src/index.ts
| const hasInitiatedSignInRef: RefObject<boolean> = useRef<boolean>(false); | ||
|
|
||
| const shouldInitiateSignIn: boolean = !isLoading && !isSignedIn && !fallback && !redirectTo; | ||
|
|
||
| // Sign-in must be initiated from an effect, never from render: it updates the provider's state, | ||
| // and a render-phase update makes React discard the render and retry it, re-running the sign-in | ||
| // on every retry. | ||
| useEffect(() => { | ||
| if (!shouldInitiateSignIn) { | ||
| // Reset so a later sign-out initiates sign-in again instead of rendering the loader forever. | ||
| hasInitiatedSignInRef.current = false; | ||
| return; | ||
| } | ||
|
|
||
| if (hasInitiatedSignInRef.current) { | ||
| return; | ||
| } | ||
|
|
||
| hasInitiatedSignInRef.current = true; | ||
|
|
||
| if (signInUrl) { | ||
| navigate(signInUrl); | ||
| return; | ||
| } | ||
| if (onSignIn) { | ||
| onSignIn(signIn, overriddenSignInOptions); | ||
| return; | ||
| } | ||
|
|
||
| const mergedParams: Record<string, unknown> | undefined = (overriddenTokenRequest ?? tokenRequest)?.params; | ||
|
|
||
| signIn( | ||
| overriddenSignInOptions ?? signInOptions, | ||
| undefined, | ||
| undefined, | ||
| undefined, | ||
| mergedParams && Object.keys(mergedParams).length > 0 ? {params: mergedParams} : undefined, | ||
| ).catch((error: unknown) => { | ||
| // Logged rather than thrown: a rejection here cannot reach React, and CallbackRoute treats | ||
| // async auth failures the same way. | ||
| const signInError: ThunderIDRuntimeError = new ThunderIDRuntimeError( | ||
| 'Sign-in failed in ProtectedRoute.', | ||
| 'ProtectedRoute-SignInError-001', | ||
| 'react-router', | ||
| error, | ||
| ); | ||
|
|
||
| logger.error(signInError.message, signInError); | ||
| }); | ||
| }, [ | ||
| shouldInitiateSignIn, | ||
| signInUrl, | ||
| onSignIn, | ||
| signIn, | ||
| signInOptions, | ||
| overriddenSignInOptions, | ||
| tokenRequest, | ||
| overriddenTokenRequest, | ||
| ]); | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect signIn()/isLoading() timing in the underlying client
ast-grep outline packages/react/src/ThunderIDReactClient.ts --items all --match 'signIn|isLoading'Repository: thunder-id/javascript-sdks
Length of output: 221
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "files:"
git ls-files | rg '(^|/)ProtectedRoute\.tsx$|ThunderIDReactClient|ThunderIDProvider|SignIn' || true
echo
echo "ProtectedRoute outline if present:"
for f in $(git ls-files | rg '(^|/)ProtectedRoute\.tsx$'); do
echo "--- $f"
ast-grep outline "$f" --items all 2>/dev/null || true
done
echo
echo "Relevant line ranges:"
for f in $(git ls-files | rg '(^|/)ProtectedRoute\.tsx$'); do
echo "--- $f lines 130-250"
sed -n '130,250p' "$f" | nl -ba -v130
done
echo
echo "Search for isLoadingSync/isLoading/loading refs:"
rg -n "isLoadingSync|function isLoading|\bisLoading\b|hasInitiatedSignInRef|shouldInitiateSignIn|onSignIn\(" packages -S --glob '*.ts' --glob '*.tsx' || trueRepository: thunder-id/javascript-sdks
Length of output: 2956
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- packages/react-router/src/components/ProtectedRoute.tsx lines 1-280"
python3 - <<'PY'
from pathlib import Path
p=Path('packages/react-router/src/components/ProtectedRoute.tsx')
print(p, p.exists(), p.stat().st_size)
for i,line in enumerate(p.read_text().splitlines(), 1):
if i <= 280:
print(f"{i:4d}: {line}")
PY
echo
echo "--- packages/react/src/ThunderIDReactClient.ts outline"
python3 - <<'PY'
from pathlib import Path
import re
p=Path('packages/react/src/ThunderIDReactClient.ts')
print('exists', p.exists(), p.stat().st_size if p.exists() else None)
text=p.read_text().splitlines()
pat=re.compile(r'(?:function\s+(\w+)|class\s+(\w+)|\s*(?:public\s+)?(?:async\s+)?(?:invoke|handleError)\b|\s*(?:async\s+)?(\w+)\s*(?::[\w\s<>,\.]+)?\s*\([^{]*\)\s*\{)')
seen=set()
for i,line in enumerate(text,1):
if any(s in line for s in ['signIn', 'isLoading', 'handleError', 'invoke', 'signOut', 'reloadState']):
print(f"{i:5d}: {line}")
PY
echo
echo "--- ThunderIDProvider relevant symbols"
p=Path('packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx')
print(p, p.exists(), p.stat().st_size if p.exists() else None)
text=p.read_text().splitlines()
for i,line in enumerate(text, 1):
if any(s in line for s in ['useIsLoading', 'setIsLoading', 'isLoadingSync', 'isSignedIn', 'signIn', 'isLoading', 'ThunderIDClient']):
print(f"{i:5d}: {line}")
echo
echo "--- packages/react/src/ThunderIDReactClient.ts relevant sections"
for start in $(python3 -c 'print("\n".join([str(i) for i,l in enumerate(Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines(),1) if "signIn" in l]))' 2>/dev/null || true); do
start=$(( ${start%.} - 10 )); if [ "$start" -lt 1 ]; then start=1; fi;
end=$(( start + 220 )); echo "--- around line $start"
python3 - <<PY
from pathlib import Path
lines=Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines()
start=max(1, int("$start")-10)
end=min(len(lines), int("$start")+220)
for i,line in enumerate(lines[start-1:end], start):
print(f"{i:5d}: {line}")
PY
doneRepository: thunder-id/javascript-sdks
Length of output: 9995
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- packages/react/src/ThunderIDReactClient.ts lines 110-235"
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines(), 1):
if 110 <= i <= 235:
print(f"{i:5d}: {line}")
PY
echo
echo "--- packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx relevant sections"
python3 - <<'PY'
from pathlib import Path
lines=Path("packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx").read_text().splitlines()
for i, line in enumerate(lines, 1):
term="isLoadingSync" in line or "useIsLoading" in line or "setIsLoading" in line or "signIn" in line or "isSignedIn" in line or "isLoading" in line
if term:
print(f"{i:5d}: {line}")
PY
echo
echo "--- packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx section around hook/provider "
python3 - <<'PY'
from pathlib import Path
lines=Path("packages/react/src/contexts/ThunderID/ThunderIDProvider.tsx").read_text().splitlines()
# crude around 100-240
for i,line in enumerate(lines,1):
if 90 <= i <= 250:
print(f"{i:5d}: {line}")
PY
echo
echo "--- Read-only behavioral probe: guard state transitions and failure path"
python3 - <<'PY'
class State:
def __init__(self, is_signed_in=False, is_loading=False, fallback=None, redirect_to=None, initiated=False):
self.is_signed_in=is_signed_in
self.is_loading=is_loading
self.fallback=fallback
self.redirect_to=redirect_to
self.has_initiated_signed_in_ref_current=initiated
self.should_initiate_sign_in=None
`@property`
def should_initiate_sign_in(self):
self.should_initiate_sign_in = not self.is_loading and not self.is_signed_in and not self.fallback and not self.redirect_to
return self.should_initiate_sign_in
def effect(action):
if not state.should_initiate_sign_in:
if state.has_initiated_signed_in_ref_current and action != "SIGN_IN":
print("RESET at", action)
state.has_initiated_signed_in_ref_current=False
return "RESET_RETIRE"
if state.has_initiated_signed_in_ref_current:
return "GUARD_BLOCKED"
state.has_initiated_signed_in_ref_current=True
return "ACTION"
for title,(init,is_loading,is_signed_in,rejected) in [
("normal flow starts", (False, False, False, None)),
("signin calls isLoadingSync(true) synchronously", (False, True, False, None)),
("after sign-in success", (True, False, True, None)),
("rejected promise after isLoading transient reset", (False, True, False, "rejected")),
("rejected promise later still loading", (False, True, False, None)),
("rejected promise later not loading (fallback needed)", (False, False, False, None)),
]:
state=State(*init)
print("---", title, "init", init, "initiated", state.has_initiated_signed_in_ref_current)
res=effect("PROVIDER_SET_LOADING_TRUE")
print(" effect", action or "PROVIDER_SET_LOADING_TRUE", "=>", res, "initiated", state.has_initiated_signed_in_ref_current)
state.is_loading=False
res=effect("PROVIDER_IS_LOADING_RESOLVED")
print(" effect", action or "PROVIDER_IS_LOADING_RESOLVED", "=>", res, "initiated", state.has_initiated_signed_in_ref_current)
state.is_signed_in=True
res=effect("PROVIDER_SET_SIGNED_IN")
print(" effect", action or "PROVIDE_SET_SIGNED_IN", "=>", res, "initiated", state.has_initiated_signed_in_ref_current)
res=effect("NEED_RETRY")
print(" effect", "NEED_RETRY", "=>", res, "initiated", state.has_initiated_signed_in_ref_current)
PYRepository: thunder-id/javascript-sdks
Length of output: 15271
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- Read-only behavioral probe: guard state transitions and failure path"
python3 - <<'PY'
class State:
def __init__(self, is_signed_in=False, is_loading=False, fallback=None, redirect_to=None, initiated=False):
self.is_signed_in=is_signed_in
self.is_loading=is_loading
self.fallback=fallback
self.redirect_to=redirect_to
self.has_initiated_signed_in_ref_current=initiated
self.should_initiate_sign_in=None
def reset_should_initiate(self):
self.should_initiate_sign_in = not self.is_loading and not self.is_signed_in and not self.fallback and not self.redirect_to
return self.should_initiate_sign_in
def effect(self, label):
self.reset_should_initiate()
if not self.should_initiate_sign_in:
if self.has_initiated_signed_in_ref_current and label != "SIGN_IN":
print(" RESET at", label)
self.has_initiated_signed_in_ref_current=False
return "RESET_RETIRE"
if self.has_initiated_signed_in_ref_current:
return "GUARD_BLOCKED"
self.has_initiated_signed_in_ref_current=True
return "ACTION"
cases = [
("normal flow starts", (False, False, False, None)),
("signin caller sets provider loading", (False, True, False, None)),
("after sign-in success", (True, False, True, None)),
("rejected promise later still loading", (False, True, False, None)),
("rejected promise becomes non-loading while still not signed in", (False, False, False, None)),
]
for title, (is_loading, is_loading_initially, is_signed_in, loading_finally) in cases:
print("---", title)
state=State(is_loading=is_loading_initially, is_signed_in=is_signed_in)
print("initial", "initiated", state.has_initiated_signed_in_ref_current)
# Initial effect before sign-in runs.
res=state.effect("INITIAL_RENDER")
print("initial effect", "=>", res, "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())
# Provider starts loading/sessions sync because signIn() called.
state.is_loading=True
res=state.effect("ENTER_LOADING_DURING_CALL")
print("enter loading effect", "=>", res, "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())
# Reject occurs.
print("rejected", "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())
# Loading resolves but user still not signed in, same sign-in remains necessary.
if loading_finally is False:
state.is_loading=False
state.is_signed_in=False
res=state.effect("LOADING_RESOLVED_POST_FAILURE")
print("loading_resolved effect", "=>", res, "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())
# User remounts or otherwise needs sign-in again.
state = State(is_loading=is_loading, is_signed_in=is_signed_in, initiated=state.has_initiated_signed_in_ref_current)
res=state.effect("NEED_RETRY")
print("retry effect", "=>", res, "initiated", state.has_initiated_signed_in_ref_current, "should_initiate", state.reset_should_initiate())
PY
echo
echo "--- ThunderIDReactClient withLoading implementation"
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines(), 1):
if any(s in line for s in ['withLoading', 'loadingState', 'async withLoading', 'function withLoading', 'isLoading()']):
print(f"{i:5d}: {line}")
PYRepository: thunder-id/javascript-sdks
Length of output: 2559
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- packages/react/src/ThunderIDReactClient.ts lines 1-75"
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path("packages/react/src/ThunderIDReactClient.ts").read_text().splitlines(), 1):
if 1 <= i <= 75:
print(f"{i:5d}: {line}")
PY
echo
echo "--- packages/javascript/src/client/ThunderIDClient.ts withLoading implementation"
P=$(git ls-files | rg 'packages/javascript/src/client/\w+Client\.ts$|packages/javascript/src/client/\w+\w+Client\.ts$' | grep -i thunderid | head -5)
printf '%s\n' "$P"
python3 - <<'PY'
from pathlib import Path
roots = [Path(r) for r in open('/tmp/coderabbit-shell-logs/shell-output-YNNT-probe-placeholder.txt', 'r') if 'packages/javascript/src/client/' in r]
PY
for p in $P; do
if grep -q 'function withLoading\|async withLoading\|withLoading' "$p"; then
echo "--- $p"
python3 - <<PY
from pathlib import Path
for i, line in enumerate(Path("$p").read_text().splitlines(), 1):
if 'withLoading' in line or 'loadingState' in line or 'isLoading()' in line:
print(f"{i:5d}: {line}")
PY
fi
doneRepository: thunder-id/javascript-sdks
Length of output: 3019
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- all withLoading definitions"
rg -n "withLoading|loadingState|isLoading:" packages -g '*.ts' -g '*.tsx' || true
echo
echo "--- packages/javascript/src imports and ThunderIDBrowserClient definition candidates"
git ls-files packages/javascript/src | rg 'ThunderID.*Client|BrowserClient|client' | head -80
echo
echo "--- packages/browser/src exports/ThunderIDBrowserClient"
git ls-files packages/browser/src | rg 'ThunderID|Client' | head -80
for f in $(git ls-files packages/browser/src | rg 'ThunderID|Client'); do
if rg -q 'class .*Client|async withLoading|function withLoading|loadingState|isLoading' "$f"; then
echo "--- $f"
rg -n -C 4 'class .*Client|async withLoading|function withLoading|loadingState|isLoading' "$f"
fi
doneRepository: thunder-id/javascript-sdks
Length of output: 18972
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- packages/browser/src/ThunderIDBrowserClient.ts lines 130-280"
python3 - <<'PY'
from pathlib import Path
for i, line in enumerate(Path("packages/browser/src/ThunderIDBrowserClient.ts").read_text().splitlines(), 1):
if 130 <= i <= 280:
print(f"{i:5d}: {line}")
PY
echo
echo "--- packages/javascript/src/ThunderIDJavaScriptClient.ts isLoading implementation and sign-in helpers"
python3 - <<'PY'
from pathlib import Path
text=Path("packages/javascript/src/ThunderIDJavaScriptClient.ts").read_text().splitlines()
for i,line in enumerate(text,1):
if 'isLoading()' in line or 'signIn' in line or 'updateSession' in line or 'navigate' in line:
print(f"{i:5d}: {line}")
PYRepository: thunder-id/javascript-sdks
Length of output: 6361
Do not reset the sign-in guard while sign-in is still needed.
Only retire hasInitiatedSignInRef when the route no longer needs sign-in (isSignedIn, fallback, or redirectTo). If isLoading resets the guard, the in-flight signIn() can set isLoadingSync(true) and then clear it before the browser unloads for the redirect, causing ProtectedRoute to call signIn() again. Also reset the guard when the failed signIn() rejects so later recovery can retry instead of remaining permanently blocked.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/react-router/src/components/ProtectedRoute.tsx` around lines 169 -
228, Update the guard reset logic in the ProtectedRoute sign-in useEffect so
isLoading changes do not clear hasInitiatedSignInRef while sign-in remains
needed; reset it only when isSignedIn, fallback, or redirectTo indicates sign-in
is no longer required. In the signIn rejection handler, clear
hasInitiatedSignInRef before logging the error so a later recovery can retry.
Fixes thunder-id/thunderid#4115 — a single console sign-in issued ~10
GET /users/me(measured: 9 and 12) instead of 1.Two independent defects, measured separately at runtime.
1.
ProtectedRouteretried its own render, re-issuing sign-in each timesignIn()was called from the render body, which updatesThunderIDProviderstate during render:Worse, the
throw new ThunderIDRuntimeError('ProtectedRoute misconfiguration.', ...)sat after theif (!isSignedIn)block and no branch inside returned. SinceisSignedInis already knownfalseat that point, it fired on every unauthenticated render, including ones the component had just handled correctly (signInUrl,onSignIn, and the default sign-in all fell through to it).React discarded each throw and re-rendered the root synchronously (
11× There was an error during concurrent rendering but React was able to recover), and every retry re-ran the render-phasesignIn(). That loop is the request burst. It self-terminated once auth state settled, which is why the bug was invisible except in the network tab.Decisions:
useEffect, guarded by a ref so it fires once per unauthenticated state. The ref resets when the state leaves "unauthenticated", so a later sign-out re-initiates rather than rendering the loader forever.isLoading→loader,isSignedIn→children,fallback,redirectTo→<Navigate>, otherwise initiate sign-in and render the loader), soProtectedRoute-Misconfiguration-001is unreachable by construction. It never detected a misconfiguration; it was a missingreturn.ProtectedRoute-SignInError-001is kept but reported, not thrown. It previously lived inside anasyncIIFE, so it could never reach React and became an unhandled rejection. It is now constructed with the same code and origin, carries the original error asdetails, and goes to the package logger — matching howCallbackRoutealready handles async auth failures.2.
ThunderIDProviderresumed the session twice per mountThe mount effect took
isAlreadySignedIn = await client.isSignedIn(), ranresumeSession()if true, then unconditionally scheduled the refresh, re-checkedisSignedIn(), and ranresumeSession()again. The pre-check can only be true when the token is valid — a state in which the re-check is also trivially true — so an already-signed-in load always paid 2/users/meand 3startAutoRefreshToken().Decisions:
startAutoRefreshToken()is still called unconditionally on every mount, before the sign-in check. Keeping it there preserves the case it exists for: when the access token expired but the refresh token is still valid,startAutoRefreshToken()refreshes immediately (timeUntilRefresh <= 0), so theisSignedIn()check that follows observes the refreshed state. A regression test covers exactly this ordering.isSignedIn()pre-check was dropped, along with the duplicateresumeSession()it guarded. The second check is retained because it is the one that can observe a silent refresh. This also matches@thunderid/vue, which has always done this as a singleif.updateSession()is now single-flight. Concurrent callers share the in-flight promise. Deliberately not a cache: the ref clears on settle, so a later call (e.g. revalidation after a profile update) fetches again.Scope decisions
<ProtectedRoute>elements and Gate mounts its own provider; making the prop-less usage correct fixes both, instead of editing 33 call sites withloader/fallbackand relying on convention for new routes.signIn()await the session sync before clearing the loading state closed the window whereisLoading === falsewhileisSignedIn === false. OnceProtectedRouteno longer loops on that window, it made no difference to the request count, so it is not included. The window itself still exists and is worth a separate look.Verification
Measured against a real console sign-in (Playwright, counting requests), building each variant separately:
ProtectedRoutefix onlyupdateSession()single-flightFinal state re-measured 3× at 1/1.
POST /oauth2/tokenunchanged (1 on login, 0 on reload). All render-phase errors gone.Tests: 11 new for
ProtectedRoute(7 fail without this change) and 5 new forThunderIDProvider.pnpm buildandpnpm format:checkpass; lint findings on the touched files are unchanged or one fewer; the 2TokenCallbackfailures and thenuxtvue-tsctooling gap are pre-existing onmain.Supporting changes: re-export
createPackageComponentLoggerfrom@thunderid/react(react-routerdepends only onreactand it was not re-exported); add@testing-library/reactdevDep, the missing*.tsxentries intsconfig.spec.json, and a.vitest-attachments/ignore toreact-router— it had no component-test setup before.Summary by CodeRabbit
Bug Fixes
New Features
Tests