token_keeper 1.2.0
token_keeper: ^1.2.0 copied to clipboard
Auth tokens, handled. Pure-Dart token manager with single-flight refresh, proactive expiry, JWT parsing with metadata extraction, background refresh timer, reactive streams, Result-based APIs, and a D [...]
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.2.0 — 2026-05-23 #
Added #
-
Token.metadata— an optionalMap<String, dynamic>field for arbitrary extra data alongside the token pair. Typical uses: tenant IDs, user IDs, feature flags, or any non-standard fields returned in the token response. Defaults toconst {}so existing call sites are unaffected. Round-trips throughtoJson/fromJson(themetadatakey is omitted from the JSON output when the map is empty). Included in==/hashCodeviaEquatable, and accepted bycopyWith. -
Token.tryParseJwt— auto-populatesmetadatafrom non-standard claims. All JWT payload claims that are not part of the RFC 7519 standard set (exp,iat,nbf,iss,sub,aud,jti) or the scope claims (scope,scp,scopes) are collected intoToken.metadata. This means custom claims liketenant_id,role,org_id, etc. are available immediately after parsing without extra plumbing. -
TokenKeeper.currentTokenStream()— aStream<Token?>that immediately emits the current stored token (the result ofpeek()) and then forwards every subsequent change fromtokenStream. Replaces the common "seed + subscribe" boilerplate:// before final initial = await keeper.peek(); keeper.tokenStream.listen(...); // after keeper.currentTokenStream().listen(...);The first emission is
nullwhen storage is empty, so the stream is always safe to listen to regardless of auth state. -
TokenKeeper.onEvent<T extends TokenEvent>()— a typed stream of lifecycle events filtered to a single event type. Cleaner than calling.where(...).cast<T>()manually:keeper.onEvent<TokenRefreshedEvent>().listen((e) { print('refreshed: ${e.token.maskedAccessToken}'); }); keeper.onEvent<TokenClearedEvent>().listen((_) => router.go('/login')); -
TokenRefreshTimer.runNow()— triggers an immediate token check outside the normal periodic schedule. The periodic timer continues unaffected. Useful when the app returns from background and you want to proactively validate / refresh the token without waiting for the next tick. A no-op when the timer has already beendisposed. -
CachingTokenStorage.refresh()— combinesinvalidate()+read()into a single async call. Returns the freshly loaded token (nullif the backing store is empty). Handy after a cross-isolate write where the in-memory cache is known to be stale:final fresh = await cachingStorage.refresh();
1.1.2 — 2026-05-15 #
Added #
Token.maskedAccessToken— partially-redacted access-token string (abcd…wxyz) safe to drop into logs and crash reports. Tokens of 8 characters or fewer are fully redacted as***.Token.expiresInSeconds([DateTime? now])— convenience getter that mirrors the OAuth 2.0expires_infield. Returns the whole-second count until expiry (0for already-expired,nullwhen expiry is unknown), so re-serializing a token to a refresh-response body is one call.CachingTokenStorage.warmup()— eagerly populates the cache from the backing store. Call once during app startup so the firstread()on the request hot path skips disk I/O.CachingTokenStorage.isCached— synchronous bool getter that distinguishes "no token stored" from "we haven't checked yet" without anawait.InMemoryTokenStorage.snapshot— synchronous peek at the persisted token; intended for test assertions that don't want toawait read().InMemoryTokenStorage.clone()— returns a new instance seeded with the current token; useful in tests when you need a decoupled copy that evolves independently.TokenKeeper.refreshIfNeeded()— alias of [getValidToken] that reads more naturally at call sites that don't immediately consume the token (e.g. background warmups, pre-flight checks). Identical behaviour and identical single-flight semantics.
Improved #
- Event
toString()—TokenRefreshedEvent,TokenClearedEvent, andRefreshFailedEventnow produce single-line, redacted debug strings instead of falling back to the defaultEquatableform. Logs and test failure output are immediately readable; the access token is shown viaToken.maskedAccessTokenso secrets stay out of log files.
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.