inspector/core/auth/index.ts
cliffhall 84a65f28d8 fix(auth): floor the shared revocation budget instead of racing zero
The shared teardown deadline was decided on `remainingMs > 0` against
`Date.now()`. Both halves of that are noise-sensitive: the deadline is
enforced by a `setTimeout`, and a wall clock at millisecond resolution
can still read a hair short of it when the loop comes round, so the next
grant inherits a fractional budget and spends it on a request that cannot
possibly complete. The same run then issues one request or two depending
on scheduling — which is what makes the "shares one deadline across
grants" test intermittent on CI (#2252).

Measure the deadline with `performance.now()` (monotonic, so an NTP step
or a suspend cannot expire or extend the budget, and sub-millisecond, so
none is lost to rounding), and skip any grant reaching the loop with less
than MIN_REVOCATION_REQUEST_BUDGET_MS left. The skipped grant is already
reported as `failed`, so nothing is silently dropped — the needless call
to the authorization server is.

The CLI's outer per-plan budget in `sendPlans` shares both the shape and
the defect, so it gets the same treatment.

One consequence worth naming: a monotonic clock hands `revokeToken` a
fractional budget, and Node's `AbortSignal.timeout` throws
`ERR_OUT_OF_RANGE` on a non-integer delay — before the fetch, so the
request would never be sent and the revocation would report that as its
failure. `revokeToken` now rounds its own budget to whole milliseconds,
which also covers any other caller passing a fractional one.

Tests: the epsilon skip is pinned by a grant that lands inside the floor
but above zero (removing the floor fails it, verified); the fractional
budget by a `revokeToken` call at 19.996ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wEXtbs8UEHUMxDEAxbs99
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-05 01:25:25 -04:00

165 lines
4 KiB
TypeScript

// Types
export type {
OAuthStep,
AuthProtocol,
OAuthClientRegistrationKind,
MessageType,
StatusMessage,
OAuthFlowState,
OAuthConnectionState,
CallbackParams,
} from "./types.js";
export {
EMPTY_OAUTH_FLOW_STATE,
authProtocolFromEnterpriseManaged,
} from "./types.js";
export {
buildOAuthConnectionState,
hasPersistedOAuthServerState,
isServerOAuthConfigured,
protocolFromOAuthConfig,
} from "./connection-state.js";
export type { BuildOAuthConnectionStateParams } from "./connection-state.js";
export { ensureCimdClientRegistration } from "./cimd.js";
export { mcpAuth, type McpAuthOptions, type McpAuthResult } from "./mcpAuth.js";
export { computeScopeUnion, isStrictScopeSuperset } from "./scopes.js";
// Custom authorization-request parameters (#2018)
export {
RESERVED_AUTHORIZATION_PARAMS,
isReservedAuthorizationParam,
authorizationParamKeyError,
applyAuthorizationParams,
} from "./authorizationParams.js";
export type { ReservedAuthorizationParam } from "./authorizationParams.js";
// Authorization/token endpoint overrides (#1906)
export {
oauthEndpointUrlError,
normalizeOAuthEndpointOverrides,
isAuthorizationServerMetadata,
applyOAuthEndpointOverrides,
withOAuthEndpointOverrides,
} from "./endpointOverrides.js";
export type { OAuthEndpointOverrides } from "./endpointOverrides.js";
export {
oidcDiscoveryCandidates,
isRfc8414OnlyMetadata,
withRfc8414OidcCompat,
} from "./oidcDiscoveryCompat.js";
// Storage
export type {
OAuthStorage,
IdpSessionState,
SaveClientInformationOptions,
} from "./storage.js";
export { getServerSpecificKey, OAUTH_STORAGE_KEYS } from "./storage.js";
// Providers
export type {
OAuthProviderConfig,
RedirectUrlProvider,
OAuthNavigation,
OAuthNavigationCallback,
} from "./providers.js";
export {
MutableRedirectUrlProvider,
ConsoleNavigation,
CallbackNavigation,
BaseOAuthClientProvider,
} from "./providers.js";
// Utilities
export {
parseHttpUrl,
parseOAuthCallbackParams,
generateOAuthState,
parseOAuthState,
generateOAuthErrorDescription,
formatOAuthFailureDetail,
isUnauthorizedError,
} from "./utils.js";
export type {
AuthChallenge,
AuthChallengeReason,
AuthChallengeOutcome,
HandleAuthChallengeOptions,
ParseAuthChallengeContext,
WwwAuthenticateBearerParams,
} from "./challenge.js";
export {
AuthChallengeError,
AuthRecoveryRequiredError,
parseAuthChallengeFromError,
parseAuthChallengeFromResponse,
parseScopeString,
parseWwwAuthenticateBearer,
unionAuthorizationScopes,
isAuthChallengeError,
isConnectAuthRecoveryError,
findNestedAuthError,
EMA_STEP_UP_PENDING_URL,
} from "./challenge.js";
export {
isStandardOAuthStepUp,
isEmaStepUp,
isStepUpConfirmation,
stepUpConfirmMessage,
stepUpFollowUpMessage,
stepUpModalTitle,
stepUpAuthorizeActionLabel,
emaStepUpInProgressMessage,
emaStepUpSuccessMessage,
emaStepUpFailureMessage,
stepUpAdditionalScopes,
stepUpInsufficientScopeMessage,
oauthPreRedirectToastCopy,
isReAuthBannerReason,
reAuthBannerMessage,
lostAuthorizationStateTitle,
lostAuthorizationStateMessage,
lostAuthorizationStateActionLabel,
issuerMismatchTitle,
issuerMismatchMessage,
issuerBindingFailureCopy,
type OAuthInteractiveAuthKind,
} from "./oauthUx.js";
export {
findIssuerBindingFailure,
type IssuerBindingFailure,
} from "./issuerBinding.js";
// Discovery
export { discoverScopes } from "./discovery.js";
// RFC 7009 token revocation (#2144)
export {
DEFAULT_REVOCATION_TIMEOUT_MS,
MIN_REVOCATION_REQUEST_BUDGET_MS,
aggregateOutcomes,
buildRevocationRequest,
revocationAuthMethods,
clearAndPlanRevocation,
executeOAuthRevocation,
revokeToken,
selectRevocableToken,
} from "./revocation.js";
export type {
ExecuteOAuthRevocationParams,
OAuthRevocationPlan,
PlanOAuthRevocationParams,
RevocationRequestParams,
RevokeTokenParams,
TokenRevocationOutcome,
TokenRevocationSkipReason,
} from "./revocation.js";
// Logging (re-exported from core/logging)
export { silentLogger } from "../logging/index.js";