@@ -96,15 +96,16 @@ export async function authenticateAndAuthorize<TParams, TSearchParams, TContext
9696 }
9797
9898 if ( options . authorization && ! isAuthorized ( auth . ability , options . authorization ) ) {
99- // Logged in but lacking the permission. By default throw a
100- // permission-denied 403 — both loader and action wrappers throw this
101- // response, so it bubbles to the nearest route ErrorBoundary, where
102- // RouteErrorDisplay renders the permission panel. Routes that prefer a
103- // redirect (e.g. credential endpoints with no UI) opt out by setting
104- // unauthorizedRedirect.
105- if ( options . unauthorizedRedirect ) {
106- return { ok : false , response : redirect ( options . unauthorizedRedirect ) } ;
99+ // Super-admin gates must not reveal that the route exists, so they redirect
100+ // away rather than render the panel. A redirect is also used by routes that
101+ // opt in via unauthorizedRedirect (e.g. credential endpoints with no UI).
102+ const isSuperGate = "requireSuper" in options . authorization ;
103+ if ( options . unauthorizedRedirect || isSuperGate ) {
104+ return { ok : false , response : redirect ( options . unauthorizedRedirect ?? "/" ) } ;
107105 }
106+ // Role-based denial: throw a permission-denied 403. Both loader and action
107+ // wrappers throw this, so it bubbles to the nearest route ErrorBoundary,
108+ // where RouteErrorDisplay renders the permission panel.
108109 return { ok : false , response : permissionDeniedResponse ( options . authorization . message ) } ;
109110 }
110111
0 commit comments