token_keeper 1.1.1
token_keeper: ^1.1.1 copied to clipboard
Auth tokens, handled. Pure-Dart token manager with single-flight refresh, proactive expiry handling, Result-based APIs, event stream and a Dio interceptor for drop-in 401 retry.
Changelog #
All notable changes to this package will be documented in this file. The format follows Keep a Changelog and this project adheres to Semantic Versioning.
1.1.1 — 2026-05-04 #
Added #
Token.requiresRefresh()— convenience method that returnstruewhen the token will expire within the next 5 minutes (configurable viawindowparameter). Simplifies proactive refresh logic without manual expiry buffer management.Token.isValidWithAllScopes(List<String>)— combines expiry and scope checks in a single call; returnstrueif the token is valid and grants all required scopes. Reduces boilerplate at request boundaries.Token.isValidWithAnyScope(List<String>)— combines expiry and scope checks in a single call; returnstrueif the token is valid and grants at least one of the specified scopes.
1.1.0 — 2026-05-04 #
Major upgrade. This release replaces the package's home-grown
Result<T>/Failure<T>types with the unifiedresilifyResult/Failuremodel so authentication errors share the same vocabulary as the rest of your networking stack. It contains breaking changes — see "Migration" below.
Added #
resilifyintegration —Result<T>,Success<T>,Error<T>, and the richFailurevalue type (with named constructorsunauthorized,network,timeout,serverError,rateLimit, …) are re-exported frompackage:token_keeper/token_keeper.dart. Callers no longer need to importresilifydirectly; it comes along for the ride.RefreshRetryConfig— wrapsresilify'sRetryHelper.retry. Same exponential / jitter /attemptTimeoutmachinery used everywhere else in the resilify ecosystem. The defaultretryIfisfailure.isRetryable, so 5xx / 408 / 429 are retried but 401 (auth dead) is not.Token.tryParseJwt(String, {String? refreshToken})— pure-Dart JWT parser (base64url + JSON, no signature verification) that auto-fillsexpiresAtfrom theexpclaim andscopesfromscope/scp/scopesclaims. Returnsnullon bad input; never throws.Token.hasScope/hasAllScopes/hasAnyScope— RFC 6749 case-sensitive scope check helpers (carried over from 1.0.x).CachingTokenStorage— decorator that wraps anyTokenStoragebackend with an in-memory cache so hot-path reads avoid disk I/O. ExposescachedToken(sync read of the cache) andinvalidate()for cross-isolate scenarios.TokenKeeper.tokenStream(Stream<Token?>) — reactive stream of token changes. Emits the newTokenafter refresh /setTokens, andnullafterclear()or unauthorized refresh failure.TokenRefreshTimer— periodic background timer that callsgetValidToken()on a configurable interval. For long-lived services that don't naturally exercise the keeper through requests.TokenKeeper.isRefreshing— synchronousbool; true while a refresh is in flight (carried over from 1.0.1).TokenKeeperInterceptor.onRefreshFailed— callback receives the resilifyFailure(carried over from 1.0.1).
Changed (BREAKING) #
- The shape of
Result<T>/Failureis now defined byresilify:Failure<T>(generic) →Failure(non-generic value type withcode,message,cause,stackTrace).Success<T>(value)→Success<T>(data)— the field name changed.Failure<T>(message: x, type: FailureType.unauthorized)→Failure.unauthorized(message: x)(or any other named constructor).- The
FailureTypeenum is removed; categorise failures by HTTPcodeor useFailure.is4xx/is5xx/isRetryablegetters.
result.fold(onSuccess:, onFailure:)(named) →result.fold(onSuccess, onError)(positional, matches resilify).result.value(onSuccess) →result.data.result.valueOrNull→result.dataOrNull.Failure<Token>inRefreshFailedEventis now plainFailure.TokenKeeper(retryPolicy: RefreshRetryPolicy.exponential(...))→TokenKeeper(retryConfig: RefreshRetryConfig.exponential(...)).
Migration (1.0.x → 1.1.0) #
// before
return const Failure(message: 'no token', type: FailureType.unauthorized);
// after
return const Error(Failure.unauthorized(message: 'no token'));
// before
if (firstAttempt.type == FailureType.unauthorized) { /* retry */ }
// after
if (firstAttempt.failure.code == 401) { /* retry */ }
// before
result.fold(onSuccess: (t) => ..., onFailure: (f) => ...);
// after
result.fold((t) => ..., (f) => ...); // positional
// or
result.when(success: (t) => ..., error: (f) => ...);
// before
TokenKeeper(retryPolicy: RefreshRetryPolicy.exponential(maxAttempts: 3));
// after
TokenKeeper(retryConfig: RefreshRetryConfig.exponential(maxAttempts: 3));
dart:core.Errorcollision —resilify'sError<T>variant shadowsdart:core.Error. If you need both in the same file, hide one:import 'dart:core' hide Error;
1.0.1 — 2026-05-02 #
Added #
Token.isValid([DateTime? now])— convenience inverse ofisExpired; alwaystruefor tokens with noexpiresAt.Token.remainingLifetime([DateTime? now])— returnsDuration?until expiry;nullfor unknown lifetime,Duration.zero(never negative) when already expired. Useful for countdown timers and progress indicators.Token.hasScope(String),Token.hasAllScopes(List<String>),Token.hasAnyScope(List<String>)— RFC 6749 case-sensitive scope-check helpers; remove repetitivescopes.containscalls from application code.Token.fromJsonOrNull(Map<String, dynamic>)— safe deserialisation factory that returnsnullinstead of throwing on malformed or corrupt JSON. Preferred at storage read boundaries.Result.getOrElse(T Function() fallback)— extracts the success value or callsfallbackfor aFailure; keeps call sites free ofswitchboilerplate for the common "or default" case.TokenKeeper.isRefreshing— synchronousboolgetter;truewhile a refresh is actively in flight. Useful for showing loading spinners without subscribing to the event stream.TokenKeeperInterceptor.onRefreshFailedcallback — optional hook invoked when a 401-triggered refresh fails. Allows navigating to login directly from the interceptor without a separateeventssubscription.
Improved #
Failure.toString()now produces a readableFailure(type: message[, cause: ...])string instead of the default Equatable dump — makes test failure output immediately actionable.TokenEventsubclasses now implementEquatable(withprops) so events can be compared with==directly in tests and reactive state layers without.runtimeTypechecks.
1.0.0 — 2026-05-02 #
Added #
Tokenmodel with JSON, equality, expiry helpers, andcopyWith.Result<T>/Success<T>/Failure<T>sealed types withFailureTypeenum (unauthorized,network,unknown).TokenStorageinterface plusInMemoryTokenStorageimplementation.TokenKeepercore with:- single-flight refresh (one in-flight refresh per keeper, even under 50+ concurrent calls),
- proactive refresh via
proactiveWindow, withValidTokenwith bounded one-shot retry onunauthorized,getValidToken,forceRefresh,setTokens,clear,peek,- lifecycle event stream (
TokenRefreshedEvent,TokenClearedEvent,RefreshFailedEvent).
TokenKeeperInterceptorfor Dio with attach + 401-refresh-and-retry.RefreshRetryPolicywith built-in exponential backoff factory.- Pluggable
TokenKeeperLoggerandClock/FixedClockfor tests. - 37 unit tests covering single-flight, proactive refresh, retry policy, events, interceptor 401 handling, and edge cases.