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
|
|
@ -36,6 +36,11 @@ import type { OAuthStorage } from "./storage.js";
|
|||
* committed to, so an unreachable or wedged authorization server must not hold
|
||||
* the teardown open — five seconds is long enough for a real endpoint on a slow
|
||||
* link and short enough that a dead one is not felt as a hang.
|
||||
*
|
||||
* It is a budget for the **whole** call, not per request: a server with several
|
||||
* issuer-bound grants revokes them sequentially against one shared deadline, so
|
||||
* the bound the user feels is this number regardless of how many grants the
|
||||
* clear is about to delete.
|
||||
*/
|
||||
export const DEFAULT_REVOCATION_TIMEOUT_MS = 5000;
|
||||
|
||||
|
|
@ -525,6 +530,14 @@ async function computeOutcome(
|
|||
|
||||
const supportedAuthMethods = revocationAuthMethods(metadata);
|
||||
const outcomes: TokenRevocationOutcome[] = [...failures];
|
||||
// ONE deadline for the whole teardown, not one per grant. `clear` deletes
|
||||
// every issuer slot, so a server with N of them would otherwise block the
|
||||
// disconnect for N × the timeout — at which point the "short timeout"
|
||||
// bounds a single request and nothing the user experiences. Grants are
|
||||
// revoked sequentially on purpose (a burst of parallel requests to one
|
||||
// authorization server is not a kindness), so the budget is shared instead.
|
||||
const timeoutMs = params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS;
|
||||
const deadlineAt = Date.now() + timeoutMs;
|
||||
for (const grant of grants) {
|
||||
// Metadata is cached once per server, not per issuer, so it describes
|
||||
// whichever authorization server was discovered last. Sending another
|
||||
|
|
@ -542,6 +555,17 @@ async function computeOutcome(
|
|||
});
|
||||
continue;
|
||||
}
|
||||
const remainingMs = deadlineAt - Date.now();
|
||||
if (remainingMs <= 0) {
|
||||
outcomes.push({
|
||||
status: "failed",
|
||||
endpoint,
|
||||
detail: `the ${timeoutMs}ms revocation budget was exhausted before this grant was attempted${
|
||||
grant.issuer === undefined ? "" : ` (issuer ${grant.issuer})`
|
||||
}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
outcomes.push(
|
||||
await revokeToken({
|
||||
endpoint,
|
||||
|
|
@ -550,7 +574,7 @@ async function computeOutcome(
|
|||
clientInformation: grant.clientInformation,
|
||||
supportedAuthMethods,
|
||||
fetchFn,
|
||||
timeoutMs: params.timeoutMs,
|
||||
timeoutMs: remainingMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue