zero_auth 0.5.0
zero_auth: ^0.5.0 copied to clipboard
Backend-agnostic auth state machine & session lifecycle for Dart/Flutter.
Changelog #
0.5.0 #
Fixed / 修复 #
validAccessToken()no longer returns an expired token. When the token had expired and the session carried no refresh token, it returned the expired value — the exact case the method exists to prevent. It now returnsnull.validAccessToken()不再返回过期令牌。 当令牌已过期且会话没有刷新令牌时, 它会返回那个过期值 —— 正是该方法本该防止的情况。现在返回null。
AuthManagerGroup.remove()cannot leak a manager.logout()may throw (for example when the store cannot be cleared), which previously skippeddispose(). Disposal now happens in afinally.AuthManagerGroup.remove()不再泄漏管理器。logout()可能抛异常(例如存储 无法清空),过去会让dispose()被跳过;现在释放放在finally中。
- Using a group after
disposeAll()is rejected withAuthException(code: 'group_disposed')instead of throwingBad state: Cannot add event after closingfrom the closed stream controller.disposeAll()is also idempotent now.disposeAll()之后使用分组会被拒绝(AuthException(code: 'group_disposed')), 不再由已关闭的 stream controller 抛出Bad state。disposeAll()现可重复调用。
restore(refreshIfExpired: false)no longer triggers a renewal behind the caller's back. An expired session restored verbatim is activated without the proactive scheduler immediately refreshing it.restore(refreshIfExpired: false)不再背着调用方触发续期。 原样恢复的过期 会话被激活时,主动调度不会立刻去刷新它。
updateSession()closes a save race: it re-checks the session on both sides of the (now possibly async) update, and clears the store again if the session was invalidated while saving, so a logout cannot leave a session behind.updateSession()补上保存竞态:在(现在可能是异步的)更新前后都重新校验, 若保存期间会话失效则再次清空存储,登出后不会残留会话。
Changed / 变更 #
- Proactive renewal retries are bounded. A failed renewal is re-armed with a
linear backoff (30s, 60s, 90s…) up to
autoRefreshMaxRetries(default 3), instead of retrying forever. Failures remain visible asAuthErroron the stream.- 主动续期重试有上限。 失败后按线性退避(30s、60s、90s…)重新排程,最多
autoRefreshMaxRetries次(默认 3),不再无限重试。失败仍以AuthError可见。
- 主动续期重试有上限。 失败后按线性退避(30s、60s、90s…)重新排程,最多
updateSession()accepts aFutureOr<AuthSession>update, so the new session can be fetched over the network first. Sync callers are unaffected.updateSession()接受FutureOr<AuthSession>,可先联网再取新会话;同步调用方不受影响。
logoutAll()signs out every account even if one of them fails, then reports the first error.logoutAll()即使某个账号失败也会继续登出其余账号,最后上报第一个错误。
Added / 新增 #
AuthSession.copyWith()— replace only the fields you pass. Passingnullkeeps the current value, as with most hand-writtencopyWith; build a new session to clear a field.AuthSession.copyWith()——只替换传入的字段。与多数手写实现一样,传null表示保留原值;要清空字段请新建会话。
AuthSession.timeUntilExpiry()andisExpiringWithin(window)— renew a little before the token actually dies.AuthSession.timeUntilExpiry()与isExpiringWithin(window)——便于在 令牌真正失效前提前续期。
autoRefreshMaxRetries— constructor knob for how many failed proactive renewals to retry (default 3).autoRefreshMaxRetries——构造参数,主动续期失败后最多重试几次(默认 3)。
AuthManagerGroup.restoreAll(dropOthers:)— drop managers for accounts that are no longer known.AuthManagerGroup.restoreAll(dropOthers:)——释放不再已知的账号管理器。
0.4.0 #
Fixed / 修复 #
restore()no longer resurrects a session after a logout. It now takes part in the same epoch guard aslogin/refresh, so a session loaded from a slowTokenStorecannot be activated after the user signed out.restore()不再在登出后「复活」会话。 它现在与login/refresh一样参与 epoch 守卫,慢速TokenStore读出的会话无法在用户登出后被激活。
- Local logout always completes. A failing
TokenStore.clear()used to leave the manager stuck inLoggingOut; the store error is now reported afterUnauthenticatedis emitted.- 本地登出必定完成。 过去
TokenStore.clear()出错会让管理器卡在LoggingOut; 现在先发出Unauthenticated,再上报存储错误。
- 本地登出必定完成。 过去
- A
TokenStore.load()failure is treated as "no session", matching the documented behaviour, instead of escaping without any state change.TokenStore.load()失败按「无会话」处理(与文档一致),不再无声抛出。
- Dropping an unrenewable session now reports why:
AuthErroris emitted beforeUnauthenticated, so a UI can tell "never signed in" apart from "session expired".- 丢弃无法续期的会话会先说明原因:先发
AuthError再发Unauthenticated, 界面可区分「从未登录」与「会话过期」。
- 丢弃无法续期的会话会先说明原因:先发
AuthSessionequality now includesclaims, so a session whose only change is inclaimsis no longer swallowed by the duplicate-emission filter.AuthSession相等性现在包含claims,仅 claims 变化的会话不再被去重逻辑吞掉。
- Overlapping authentication flows are rejected (
auth_flow_in_progress) instead of racing to overwrite the session.- 重叠的登录流程会被拒绝(
auth_flow_in_progress),不再争抢覆盖会话。
- 重叠的登录流程会被拒绝(
Err.appExceptionexposes the mapped domain error; theerrorfield staysObjectand its docs no longer over-promiseAppException.- 新增
Err.appException暴露映射后的领域错误;error仍为Object,文档不再 过度承诺。
- 新增
- Docs: the
AuthStrategycontract shown on the site used pre-0.3.0 signatures (login(String, String)). It now matches the real API.- 文档:站上展示的
AuthStrategy契约仍是 0.3.0 之前的签名(login(String, String)), 现已与真实 API 一致。
- 文档:站上展示的
Changed / 变更 #
- Operations after
dispose()throwAuthException(code: 'manager_disposed')instead of silently doing nothing.dispose()之后的操作会抛出AuthException(code: 'manager_disposed'), 而非静默无作为。
- A failed proactive refresh is re-armed after
autoRefreshRetryDelay(default 30s) while a session still exists, so a transient error no longer stops renewal silently.- 主动刷新失败后会重新排程(
autoRefreshRetryDelay,默认 30 秒),只要会话仍在, 瞬时错误就不会让续期默默停止。
- 主动刷新失败后会重新排程(
SessionHandlecarries an optionalrefreshTokenin addition touserId.SessionHandle除userId外还携带可选的refreshToken。
Added / 新增 #
-
AuthManagerGroup— an optional coordination layer that keeps one [AuthManager] per account, so several accounts can stay signed in at once and you can switch between them. It exposesforAccount,switchTo,restoreAll,removeanddisposeAll, mirrors the active account throughcurrent/state/currentSession, and implementsAuthTokenSourceso interceptors always read the active account's token.AuthManagerGroup——可选的协调层,为每个账号持有一个 [AuthManager],从而让多 个账号同时保持登录并可切换。它提供forAccount、switchTo、restoreAll、remove、disposeAll,通过current/state/currentSession反映激活账号, 并实现AuthTokenSource,使拦截器始终读到激活账号的令牌。
-
Multi-account is deliberately opt-in:
AuthManagerremains single-session, because "who is logged in?" and "which of these accounts is active?" are different questions. Existing code keeps working unchanged; nothing to migrate.- 多账号被刻意设计为可选:
AuthManager仍是单会话,因为「谁登录了?」与 「这些账号里哪个是激活的?」是两个不同的问题。现有代码无需任何改动。
- 多账号被刻意设计为可选:
-
updateSession(...)— replaces the active session without a re-login, for profile updates or refreshed claims. ThrowsNoActiveSessionExceptionwhen nothing is signed in.updateSession(...)——在不重新登录的情况下替换活动会话,用于资料更新或刷新 claims。未登录时抛出NoActiveSessionException。
-
Optional capability interfaces —
SupportsPasswordReset,SupportsPasswordChangeandSupportsReauthentication, detected withAuthManager.supports<T>()so the four-method contract stays intact.- 可选能力接口——
SupportsPasswordReset、SupportsPasswordChange、SupportsReauthentication,可用AuthManager.supports<T>()检测,从而保持 四方法契约不变。
- 可选能力接口——
-
onStateChanged— optional callback invoked for every emitted state, for logging or analytics without subscribing to the stream.onStateChanged——可选回调,每次发出状态时触发,便于无需订阅流即可做日志或埋点。
-
autoRefreshRetryDelay— constructor knob controlling how soon a failed proactive renewal is re-armed (default 30s).autoRefreshRetryDelay——构造参数,控制主动续期失败后多久重新排程(默认 30 秒)。
-
AuthManagerGroup.addAccountandlogoutAll()— explicit registration and signing out every account at once.AuthManagerGroup.addAccount与logoutAll()——显式注册账号,以及一次性 登出所有账号。
-
Example:
AuthRetryInterceptor— retries a request once after a transparent refresh, for backends that return 401 on an early-revoked token.- 示例:新增
AuthRetryInterceptor——在透明续期后重试一次请求,适用于令牌被提前 吊销时返回 401 的后端。
- 示例:新增
Docs / 文档 #
- New Multi-Account cookbook covering both shapes: switching between
accounts (logout + login is usually enough) and genuinely concurrent accounts
(use
AuthManagerGroup).- 新增多账号 cookbook,覆盖两种形态:账号切换(登出再登录通常足够)与真正的
多账号并存(使用
AuthManagerGroup)。
- 新增多账号 cookbook,覆盖两种形态:账号切换(登出再登录通常足够)与真正的
多账号并存(使用
0.3.0 #
Added / 新增 #
loginWithescape hatch — adopt a session produced by any flow you drive yourself (third-party OAuth, magic links, passkeys, biometric unlock) while the manager keeps owning persistence, the state machine and proactive refresh.loginWith逃生口——接纳由你自行驱动的任意流程(第三方 OAuth、魔法链接、 Passkey、生物识别解锁)所产生的会话,而持久化、状态机与主动刷新仍由管理器负责。
validAccessToken()— returns a token guaranteed not to be expired, refreshing first when needed (reusing the single-flight refresh). Ideal for HTTP interceptors; returnsnullwhen unauthenticated.validAccessToken()——返回保证未过期的令牌,必要时先续期(复用单飞刷新)。 非常适合 HTTP 拦截器;未认证时返回null。
- Typed auth exceptions —
InvalidCredentialsException,SessionExpiredException,NoActiveSessionException,RefreshTokenMissingExceptionandUnexpectedAuthException, plusmapAuthFailure, which maps your strategy'sAuthException.codeonto them and preserves any vocabulary it does not recognise.- 类型化认证异常——新增
InvalidCredentialsException、SessionExpiredException、NoActiveSessionException、RefreshTokenMissingException、UnexpectedAuthException, 以及mapAuthFailure:它按你策略里AuthException.code映射为具体子类,并保留 无法识别的自定义错误类型。
- 类型化认证异常——新增
- Refresh failure policy —
AuthManager.refreshFailurePolicy(defaults todefaultRefreshFailurePolicy) decides whether a failed refresh ends the session: unrecoverable failures sign the user out, transient ones keep it.- 刷新失败策略——
AuthManager.refreshFailurePolicy(默认为defaultRefreshFailurePolicy)决定一次刷新失败是否终止会话:不可恢复的失败会让 用户登出,瞬时故障则保留会话。
- 刷新失败策略——
clockinjection — override the time source used for expiry maths and proactive scheduling, for deterministic tests and to tolerate device clock skew.clock注入——可覆盖过期计算与主动刷新调度所用的时间源,用于确定性测试与 容忍设备时钟偏移。
AuthState.isBusy—truewhileAuthenticating,RefreshingorLoggingOut; handy for spinners and disabling buttons.AuthState.isBusy——在Authenticating、Refreshing、LoggingOut期间为true,可用于加载态与禁用按钮。
Changed / 变更 #
- BREAKING: two new
AuthStatesubtypes.RefreshingandLoggingOutjoin the sealed hierarchy, so exhaustiveswitchstatements must handle them. Preferstate.isAuthenticated/state.isBusy, which stay correct as states evolve.- 破坏性:
AuthState新增两个子类。Refreshing与LoggingOut加入密封层级, 穷举switch必须处理它们。建议改用state.isAuthenticated/state.isBusy, 这样后续状态演进也不会失效。
- 破坏性:
- BREAKING:
isAuthenticatedsemantics. It is nowtrueforRefreshingas well, so renewing a token no longer unmounts signed-in UI mid-refresh.- 破坏性:
isAuthenticated语义变更。 它在Refreshing期间同样为true, 令牌续期不再让已登录界面中途被卸载。
- 破坏性:
restore()heals expired sessions. An expired persisted session now triggers a refresh; if it cannot be renewed (or there is no refresh token) the store is cleared and the manager lands onUnauthenticated. PassrefreshIfExpired: falseto restore it verbatim as before.restore()会修复过期会话。 持久化会话若已过期会先触发刷新;若无法续期 (或没有刷新令牌),则清空存储并落到Unauthenticated。传refreshIfExpired: false可沿用过去的原样恢复行为。
refresh()emitsRefreshingwhile in flight, and throws the newNoActiveSessionException/RefreshTokenMissingExceptionwhen it cannot start.refresh()进行中会发出Refreshing,并在无法启动时抛出新的NoActiveSessionException/RefreshTokenMissingException。
- Consecutive duplicate states are suppressed, so listeners only rebuild on a
real change.
- 抑制连续重复的状态,监听器只会在真正变化时重建。
Fixed / 修复 #
- Proactive auto-refresh no longer leaks an unhandled async error when the
background refresh fails.
- 后台主动刷新失败时,不再泄漏未处理的异步错误。
- A
refresh()orlogin()landing afterlogout()/dispose()no longer resurrects the session; late results are dropped by an epoch guard.- 迟到的
refresh()/login()结果不再让会话「复活」;由 epoch 守卫丢弃。
- 迟到的
- A failed
refresh()no longer leaves the manager silently authenticated with a stale token: the failure policy now lands it onUnauthenticatedor back on the previous session, always after reportingAuthError.refresh()失败后不再「静默保持登录并持有过期令牌」:失败策略会在上报AuthError之后,落到Unauthenticated或回到上一个会话。
- Failures thrown by an
AuthStrategykeep their own vocabulary when the code is unrecognised, instead of being flattened into a generic auth exception.AuthStrategy抛出的失败在 code 无法识别时会保留其原有类型,而不再被压平为 通用的认证异常。
0.2.0 #
Added / 新增 #
- Session (de)serialization —
AuthSessionnow exposestoJson()/AuthSession.fromJson()(andRefreshTokengains matchingtoJson()/fromJson()), so a session can be persisted to disk or secure storage and rehydrated losslessly across app restarts.nullfields are omitted andclaimsround-trips as a plain map.- 会话(反)序列化——
AuthSession现提供toJson()/AuthSession.fromJson()(RefreshToken也增加对应的toJson()/fromJson()),会话可被持久化到 磁盘或安全存储,并在应用重启后无损恢复。null字段会被省略,claims作为普通映射原样往返。
- 会话(反)序列化——
- Proactive auto-refresh —
AuthManageraccepts a new optionalautoRefreshAheadduration. When a session carries both anexpiresAtand a refresh token, the manager schedules a single-flightrefresh()that many minutes before expiry, so callers rarely hit an expired access token. Disabled by default (backwards compatible).- 临近过期自动刷新——
AuthManager新增可选参数autoRefreshAhead。当会话同时 带有expiresAt与刷新令牌时,管理器会在过期前该时长调度一次单飞refresh(), 调用方几乎不会撞上过期的访问令牌。默认关闭,向后兼容。
- 临近过期自动刷新——
Example / 示例 #
- Added
example/lib/json_token_store.dart— adart:iofile-backedTokenStorebuilt on the new serialization, for server / CLI / desktop.- 新增
example/lib/json_token_store.dart——基于新序列化、面向服务端 / CLI / 桌面的dart:io文件型TokenStore。
- 新增
0.1.0 #
Added / 新增 #
- Auth state machine —
AuthManagerdrives the explicitUnauthenticated → Authenticating → Authenticated → AuthErrorlifecycle and broadcasts it on aStream<AuthState>that replays the latest value to new listeners, so a widget can render the correct screen on its first frame.- 认证状态机——
AuthManager驱动显式的Unauthenticated → Authenticating → Authenticated → AuthError生命周期, 并通过「重放最近值」的Stream<AuthState>广播,使 widget 在首帧即可渲染正确界面。
- 认证状态机——
- Backend boundary (
AuthStrategy) — a single four-method interface (login/register/logout/refresh) is all a backend has to implement. The core ships no HTTP, no SDK and no native code, so REST, gRPC, Firebase or a private RPC are equally valid targets.- 后端边界(
AuthStrategy)——后端只需实现login/register/logout/refresh四个方法。内核不含任何 HTTP、SDK 或原生代码,因此 REST、gRPC、Firebase 或自有 RPC 均可平等接入。
- 后端边界(
- Silent restore & single-flight refresh —
restore()rehydrates the persisted session at startup, and concurrentrefresh()callers share one in-flight request instead of stampeding the backend; a failed refresh clears the session and emitsAuthError.- 静默恢复与单飞刷新——
restore()在启动时恢复持久化会话;并发的refresh()调用方共享同一次进行中的请求,而不会同时冲击后端;刷新失败会清空会话并发出AuthError。
- 静默恢复与单飞刷新——
- Pluggable persistence (
TokenStore) — the only persistence surface issave/load/clear.InMemoryTokenStoreships in the core; aflutter_secure_storage-backed reference implementation lives inexample/lib/secure_token_store.dartfor production use.- 可插拔持久化(
TokenStore)——唯一的持久化接口是save/load/clear。 内核自带InMemoryTokenStore;生产可用的flutter_secure_storage参考实现位于example/lib/secure_token_store.dart。
- 可插拔持久化(
- Strongly-typed session (
AuthSession) — carries access token, refresh token, expiry (isExpired), user id, display name and raw claims, so callers never parse token payloads by hand.- 强类型会话(
AuthSession)——携带访问令牌、刷新令牌、过期时间(isExpired)、 用户 ID、显示名与原始 claims,调用方无需手工解析令牌载荷。
- 强类型会话(
- Unified errors — every domain failure is mapped to
AppExceptionfrom this package's error kernel (AuthExceptionfor auth-specific cases), and aResult<T>wrapper is available for explicitOk/Errhandling. RawExceptions never cross the public surface.- 统一错误——所有领域失败都映射为本包错误内核的
AppException(认证相关场景为AuthException),并提供Result<T>以支持显式的Ok/Err处理。 裸Exception绝不会跨越公共 API 边界。
- 统一错误——所有领域失败都映射为本包错误内核的
- Network integration (
AuthTokenSource) —AuthManageritself is anAuthTokenSource, so a Dio interceptor can attachAuthorization: Bearer <token>without depending on the manager. A ready-to-use interceptor lives inexample/lib/dio_interceptor.dart.- 网络集成(
AuthTokenSource)——AuthManager本身即是一个AuthTokenSource, 因此 Dio 拦截器可在不依赖管理器的前提下附加Authorization: Bearer <token>。 开箱可用的拦截器位于example/lib/dio_interceptor.dart。
- 网络集成(
- Runnable example & demo backend —
example/is a full Flutter app (Android / iOS / Web / Windows) that exercises login, refresh, logout and a protectedGET /mecall.server/is a zero-dependencydart:iobackend (dart run bin/server.dart) exposing/login,/refresh,/logoutand/me; any username works and the password must beb, so both the success and theAuthErrorpath are reproducible without external infrastructure.- 可运行示例与演示后端——
example/是完整的 Flutter App (Android / iOS / Web / Windows),覆盖登录、刷新、登出与受保护的GET /me调用;server/是零依赖的dart:io后端(dart run bin/server.dart),提供/login、/refresh、/logout与/me;用户名任意、密码必须是b, 因此无需外部基础设施即可复现成功与AuthError两条路径。
- 可运行示例与演示后端——