token_keeper 1.1.2 copy "token_keeper: ^1.1.2" to clipboard
token_keeper: ^1.1.2 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.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.0 expires_in field. Returns the whole-second count until expiry (0 for already-expired, null when 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 first read() 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 an await.
  • InMemoryTokenStorage.snapshot — synchronous peek at the persisted token; intended for test assertions that don't want to await 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, and RefreshFailedEvent now produce single-line, redacted debug strings instead of falling back to the default Equatable form. Logs and test failure output are immediately readable; the access token is shown via Token.maskedAccessToken so secrets stay out of log files.

1.1.1 — 2026-05-04 #

Added #

  • Token.requiresRefresh() — convenience method that returns true when the token will expire within the next 5 minutes (configurable via window parameter). Simplifies proactive refresh logic without manual expiry buffer management.
  • Token.isValidWithAllScopes(List<String>) — combines expiry and scope checks in a single call; returns true if 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; returns true if 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 unified resilify Result / Failure model so authentication errors share the same vocabulary as the rest of your networking stack. It contains breaking changes — see "Migration" below.

Added #

  • resilify integrationResult<T>, Success<T>, Error<T>, and the rich Failure value type (with named constructors unauthorized, network, timeout, serverError, rateLimit, …) are re-exported from package:token_keeper/token_keeper.dart. Callers no longer need to import resilify directly; it comes along for the ride.
  • RefreshRetryConfig — wraps resilify's RetryHelper.retry. Same exponential / jitter / attemptTimeout machinery used everywhere else in the resilify ecosystem. The default retryIf is failure.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-fills expiresAt from the exp claim and scopes from scope / scp / scopes claims. Returns null on 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 any TokenStorage backend with an in-memory cache so hot-path reads avoid disk I/O. Exposes cachedToken (sync read of the cache) and invalidate() for cross-isolate scenarios.
  • TokenKeeper.tokenStream (Stream<Token?>) — reactive stream of token changes. Emits the new Token after refresh / setTokens, and null after clear() or unauthorized refresh failure.
  • TokenRefreshTimer — periodic background timer that calls getValidToken() on a configurable interval. For long-lived services that don't naturally exercise the keeper through requests.
  • TokenKeeper.isRefreshing — synchronous bool; true while a refresh is in flight (carried over from 1.0.1).
  • TokenKeeperInterceptor.onRefreshFailed — callback receives the resilify Failure (carried over from 1.0.1).

Changed (BREAKING) #

  • The shape of Result<T> / Failure is now defined by resilify:
    • Failure<T> (generic) → Failure (non-generic value type with code, 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 FailureType enum is removed; categorise failures by HTTP code or use Failure.is4xx / is5xx / isRetryable getters.
  • result.fold(onSuccess:, onFailure:) (named) → result.fold(onSuccess, onError) (positional, matches resilify).
  • result.value (on Success) → result.data. result.valueOrNullresult.dataOrNull.
  • Failure<Token> in RefreshFailedEvent is now plain Failure.
  • 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.Error collisionresilify's Error<T> variant shadows dart: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 of isExpired; always true for tokens with no expiresAt.
  • Token.remainingLifetime([DateTime? now]) — returns Duration? until expiry; null for 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 repetitive scopes.contains calls from application code.
  • Token.fromJsonOrNull(Map<String, dynamic>) — safe deserialisation factory that returns null instead of throwing on malformed or corrupt JSON. Preferred at storage read boundaries.
  • Result.getOrElse(T Function() fallback) — extracts the success value or calls fallback for a Failure; keeps call sites free of switch boilerplate for the common "or default" case.
  • TokenKeeper.isRefreshing — synchronous bool getter; true while a refresh is actively in flight. Useful for showing loading spinners without subscribing to the event stream.
  • TokenKeeperInterceptor.onRefreshFailed callback — optional hook invoked when a 401-triggered refresh fails. Allows navigating to login directly from the interceptor without a separate events subscription.

Improved #

  • Failure.toString() now produces a readable Failure(type: message[, cause: ...]) string instead of the default Equatable dump — makes test failure output immediately actionable.
  • TokenEvent subclasses now implement Equatable (with props) so events can be compared with == directly in tests and reactive state layers without .runtimeType checks.

1.0.0 — 2026-05-02 #

Added #

  • Token model with JSON, equality, expiry helpers, and copyWith.
  • Result<T> / Success<T> / Failure<T> sealed types with FailureType enum (unauthorized, network, unknown).
  • TokenStorage interface plus InMemoryTokenStorage implementation.
  • TokenKeeper core with:
    • single-flight refresh (one in-flight refresh per keeper, even under 50+ concurrent calls),
    • proactive refresh via proactiveWindow,
    • withValidToken with bounded one-shot retry on unauthorized,
    • getValidToken, forceRefresh, setTokens, clear, peek,
    • lifecycle event stream (TokenRefreshedEvent, TokenClearedEvent, RefreshFailedEvent).
  • TokenKeeperInterceptor for Dio with attach + 401-refresh-and-retry.
  • RefreshRetryPolicy with built-in exponential backoff factory.
  • Pluggable TokenKeeperLogger and Clock / FixedClock for tests.
  • 37 unit tests covering single-flight, proactive refresh, retry policy, events, interceptor 401 handling, and edge cases.
0
likes
160
points
359
downloads

Documentation

API reference

Publisher

verified publisherhimanshulahoti.is-a.dev

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#authentication #token #oauth #dio #interceptor

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

dio, equatable, meta, resilify

More

Packages that depend on token_keeper