refactor: one shared reporter for the terminal refusal (review round 4)

In round 2 I claimed a single wrapper "stops a future path omitting the banner
clear". That was only true inside useOAuthRecovery. useConnectionLifecycle
hand-wrote the notice-plus-scoped-clear pair three times, so the exact failure
mode I said was impossible was one careless edit away — in the PR that claimed
it.

reportTerminalInsecureTokenEndpoint now lives in lib/ and both hooks route
through it; zero hand-written pairs remain. It is generic over the banner shape
rather than importing ReAuthBannerState, since useOAuthRecovery already imports
this module and naming its type would close a cycle. All it needs is a serverId
to compare.

The helper's header also still said "the three OAuth failure paths". There are
seven. Rather than update the count I removed the enumeration and recorded why:
a list of callers is a comment that rots on the next round, which is what just
happened to it.

Also fixes "a /etc/hosts" -> "an /etc/hosts".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MvNbmAtQuWhDvzi4yXDju
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-07 10:59:56 -04:00
commit c2d566ad8d

View file

@ -1,17 +1,18 @@
/**
* Surfaces the SDK's terminal token-endpoint refusal to post credentials to a non-TLS token
* endpoint as the terminal configuration error it is (#2280).
* Surfaces the SDK's refusal to post credentials to a non-TLS token endpoint as
* the terminal configuration error it is (#2280).
*
* Lives in `lib/` rather than `utils/` because showing a notification is a side
* effect; the copy it renders is pure and lives in `@inspector/core/auth`.
*
* Shaped as a claim-or-decline predicate rather than a plain `show(...)` so the
* three OAuth failure paths that need it — the connect handshake, the post-
* redirect callback, and the re-auth banner funnel — can each spend one line on
* it and keep their existing fall-through intact:
* Shaped as a claim-or-decline predicate rather than a plain `show(...)` so
* every OAuth failure path that needs it can spend one line and keep its own
* fall-through intact. Deliberately not enumerating the callers here: they have
* gone from three to seven over this PR's review, and a list is a comment that
* rots on the next one.
*
* ```ts
* if (showInsecureTokenEndpointNotice(err, server.name)) return;
* if (reportTerminalInsecureTokenEndpoint({ err, serverId: id, serverName, setReAuthBanner })) return;
* ```
*
* `autoClose: false` matches the other non-recoverable OAuth notices (issuer
@ -22,6 +23,7 @@
* reading. Don't describe this as non-dismissible.
*/
import type { Dispatch, SetStateAction } from "react";
import { notifications } from "@mantine/notifications";
import { findInsecureTokenEndpoint } from "@inspector/core/auth/insecureTokenEndpoint.js";
import {
@ -35,6 +37,48 @@ import {
* @returns `true` when it was handled (the caller should stop), `false` when
* `err` is some other failure and the caller's normal handling applies.
*/
/**
* Report the refusal **and** clear the re-auth banner for that server.
*
* This is the form every caller should use. The notice and the scoped banner
* clear are one invariant, not two steps: a banner left behind carries a
* Re-authenticate button as dead as the one this declines to offer, and the
* user cannot tell which failure it belongs to. Keeping them together in one
* place is what stops a future path doing half of it — an earlier revision made
* that claim while hand-writing the pair at three call sites in a second hook,
* which is exactly how it goes wrong.
*
* The clear is scoped to `serverId` and applied as a functional update: these
* paths are asynchronous, so a late continuation for server A must not erase a
* banner server B raised in the meantime.
*
* Generic over the banner shape rather than importing `ReAuthBannerState`:
* `useOAuthRecovery` already imports this module, so naming its type here would
* close a cycle. All this needs is a `serverId` to compare.
*
* @returns `true` when it was handled (the caller should stop), `false` when
* `err` is some other failure and the caller's normal handling applies.
*/
export function reportTerminalInsecureTokenEndpoint<
TBanner extends { serverId: string },
>({
err,
serverId,
serverName,
setReAuthBanner,
}: {
err: unknown;
serverId: string | undefined;
serverName?: string;
setReAuthBanner: Dispatch<SetStateAction<TBanner | null>>;
}): boolean {
if (!showInsecureTokenEndpointNotice(err, serverName)) {
return false;
}
setReAuthBanner((prev) => (prev && prev.serverId === serverId ? null : prev));
return true;
}
export function showInsecureTokenEndpointNotice(
err: unknown,
serverName?: string,