fix(auth): bound the whole teardown, and scope the TUI clear to its server (#2144)
Copilot review round 7. - The five-second timeout was per request while grants are revoked sequentially, so a server with N issuer slots could block the disconnect for N x 5s — the "short timeout" bounded a request rather than anything the user feels. It is now one budget for the whole call: each grant gets the remaining time, and a grant reached after it is exhausted is reported as never attempted rather than starting a fresh five seconds. `--relogin` shares one budget across its two key spellings for the same reason. - `AuthTab` called the same settle handler on rejection, so a failed local clear or disconnect announced "OAuth state cleared". A rejection is not a revocation failure — those come back as outcomes — so it now has its own path and reports the real error. The completion is also scoped: a clear carries the server it started on and an attempt number, and a switch retires it, so server A's confirmation cannot appear under B. - `clearOAuthAttemptRef` is now retired by the server-switch effect alongside `disconnectAttemptRef`; without that an A -> B -> A round trip left both the token and the captured name matching again. `handleClearOAuth` also revalidates after `await disconnectInspector()`, since a switch during that second await would have put the revision bump on the new selection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
56ac83051b
commit
43c817c8e9
1 changed files with 198 additions and 13 deletions
|
|
@ -3,6 +3,7 @@ import {
|
|||
resetNodeOAuthStorageCache,
|
||||
} from "@inspector/core/auth/node/storage-node.js";
|
||||
import {
|
||||
DEFAULT_REVOCATION_TIMEOUT_MS,
|
||||
revokeStoredOAuthTokens,
|
||||
type TokenRevocationOutcome,
|
||||
} from "@inspector/core/auth/revocation.js";
|
||||
|
|
@ -92,13 +93,26 @@ async function revokeStoredKeys(
|
|||
keys: string[],
|
||||
): Promise<TokenRevocationOutcome | undefined> {
|
||||
const fetchFn = createProxyFetch() ?? fetch;
|
||||
// One budget across both keys, for the same reason `revokeStoredOAuthTokens`
|
||||
// shares one across grants: two keys would otherwise double the wait a user
|
||||
// feels before `--relogin` gets on with the local delete.
|
||||
const deadlineAt = Date.now() + DEFAULT_REVOCATION_TIMEOUT_MS;
|
||||
let reported: TokenRevocationOutcome | undefined;
|
||||
let lastSkip: TokenRevocationOutcome | undefined;
|
||||
for (const key of new Set(keys)) {
|
||||
const remainingMs = deadlineAt - Date.now();
|
||||
if (remainingMs <= 0) {
|
||||
reported ??= {
|
||||
status: "failed",
|
||||
detail: `the ${DEFAULT_REVOCATION_TIMEOUT_MS}ms revocation budget was exhausted before "${key}" was attempted`,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
const outcome = await revokeStoredOAuthTokens({
|
||||
serverUrl: key,
|
||||
storage,
|
||||
fetchFn,
|
||||
timeoutMs: remainingMs,
|
||||
});
|
||||
if (outcome.status === "skipped" && outcome.reason === "no_tokens") {
|
||||
lastSkip = outcome;
|
||||
|
|
|
|||
Loading…
Reference in a new issue