zero_auth 1.1.0
zero_auth: ^1.1.0 copied to clipboard
Backend-agnostic auth state machine & session lifecycle for Dart/Flutter.
Changelog #
1.1.0 #
Added / 新增 #
onObserverError— called whenonStateChangedthrows, so a broken logger or analytics sink is reported instead of failing silently. Silent when omitted, and the state machine is never affected either way.AuthManagerGroupforwards an account-tagged variant to every manager it creates.onObserverError—— 当onStateChanged抛异常时调用,使坏掉的日志 / 埋点被 上报而不是静默失败;不传则保持静默,两种方式都不会影响状态机。AuthManagerGroup会把带账号标记的版本转发给它创建的每个管理器。
clockSkewFraction— capsclockSkewat this fraction of the observed token lifetime (default0.25), so a backend handing out very short-lived tokens does not turn one request into one renewal. A 5–15 minute access token is unaffected; passdouble.infinityto disable the cap.clockSkewFraction—— 把clockSkew钳制为观测到的令牌寿命的这个比例 (默认0.25),避免签发极短寿命令牌的后端把「一次请求」变成「一次续期」。5–15 分钟的访问令牌不受影响;传double.infinity可关闭该上限。
AuthManagerGroup.restoreAll(parallel:)— restore every account concurrently instead of one after another, so startup cost becomes the slowest store rather than the sum of them. Off by default; error reporting is unchanged (the first failure, inaccountIdsorder, is still thrown at the end).AuthManagerGroup.restoreAll(parallel:)—— 并发恢复所有账号,使启动耗时从 「各存储耗时之和」变为「最慢的那个」。默认关闭;错误上报行为不变(仍会按accountIds顺序抛出第一个失败)。
Changed / 变更 #
- A failing
TokenStore.save()is retried once before the failure is reported, and that failure now carries a dedicatedcode(session_persist_failed), so "could not persist" can be told apart from "rejected".TokenStore.save()失败时会先重试一次再上报,且该失败带有专门的code(session_persist_failed),便于区分「存不下」与「被拒绝」。
refresh()throws its pre-conditions synchronously.NoActiveSessionExceptionandRefreshTokenMissingExceptionnow leave the same way as themanager_disposedguard instead of coming back as a failedFuture. For a caller thatawaits, nothing changes — but one that forgets toawaitnow sees the error at the call site rather than an unhandled async error.refresh()改为同步抛出前置条件失败。NoActiveSessionException与RefreshTokenMissingException现在与manager_disposed守卫走同一条出口,而不再 以失败的Future返回。对await的调用方没有任何变化;但忘记await的调用方 现在会在调用点看到错误,而不是一个未处理的异步错误。
- Every read on
AuthManagerGroupis rejected afterdisposeAll().accountIds,activeId,active,current,currentSession,accessToken,stateandvalidAccessToken()now throwAuthException(code: 'group_disposed'), matchingstate— which already did — and the single-managermanager_disposedcontract.AuthManagerGroup在disposeAll()之后拒绝所有读取。accountIds、activeId、active、current、currentSession、accessToken、state与validAccessToken()现在都会抛AuthException(code: 'group_disposed'),与早已 如此的state保持一致,也与单管理器的manager_disposed约定一致。
Fixed / 修复 #
- A transient failure to persist a session no longer leaves the device and the
backend out of sync. The backend rotates first and the device stores
afterwards, so a lost write used to leave a dead refresh token behind — which a
rotation-aware backend answers by revoking the whole family and signing the user
out on the next start.
- 持久化会话时的瞬时失败不再让设备与后端失同步。 后端先轮换、设备后存储,因此一次 丢失的写入过去会留下一个已死的刷新令牌 —— 支持轮换的后端会据此吊销整个 family,让 用户在下次启动时被登出。
- Extreme timing interleavings now have regression tests. "A renewal landing
after
logout()" and "a renewal still in flight whendispose()arrives" are covered bytest/auth_manager_race_test.dart, which gates the renewal behind aCompleterinstead of racing the event loop.- 极端时序交错现在有了回归测试。 「续期在
logout()之后落地」与「dispose()到达时续期仍在飞行中」由test/auth_manager_race_test.dart覆盖,它用Completer挡住续期,而不是与事件循环赛跑。
- 极端时序交错现在有了回归测试。 「续期在
1.0.0 #
First stable release / 首个稳定版本. From here on the public surface follows
strict semver: a 1.x release never breaks it, and every new symbol is additive.
从本版本起,公共 API 严格遵循语义化版本:1.x 绝不做破坏性变更,新增能力一律以
追加方式提供。
Fixed / 修复 #
restore()no longer signs the user out on a transient renewal failure. An expired persisted session plus a network hiccup used to clear the store and land onUnauthenticated— the opposite of whatrefreshFailurePolicypromises. A transient failure now keeps the persisted session so the next call can retry; only a terminal one clears it.restore()不再因瞬时续期失败而让用户登出。 过去「持久化会话已过期 + 网络抖动」 会清空存储并落到Unauthenticated,与refreshFailurePolicy的承诺正好相反。现在 瞬时失败会保留持久化会话以便下次重试,只有终局失败才会清空。
- A late refresh can no longer overwrite a newer session.
login,register,loginWithandupdateSessionnow join the epoch guard, so a renewal started before them is aborted instead of replacing the session they just installed (or writing a token minted from an already-rotated refresh token).- 迟到的刷新再也无法覆盖更新的会话。
login、register、loginWith与updateSession现在都参与 epoch 守卫:在它们之前启动的续期会被中止,而不是替换 刚安装的会话(也不会写回由已轮换的刷新令牌换来的令牌)。
- 迟到的刷新再也无法覆盖更新的会话。
- A renewal no longer erases who is signed in. Backends that return tokens only
used to drop
userId/displayName/claims— which also emptied theSessionHandle.userIdsent on logout. The identity now carries over.- 续期不再抹掉「谁在登录」。 只返回令牌的后端过去会丢失
userId/displayName/claims,连带让登出时发送的SessionHandle.userId变空。现在身份字段会被保留。
- 续期不再抹掉「谁在登录」。 只返回令牌的后端过去会丢失
mapAuthFailurehonours thecodeof a bareAuthFail. Throwing the documented domain failure type no longer flattens every case intoUnexpectedAuthException.mapAuthFailure现在认得裸AuthFail的code。 抛出文档推荐的领域失败类型, 不再把所有情况都压成UnexpectedAuthException。
claimsare compared structurally. Nested maps and lists participate in equality (and hashing), so a change buried insideclaimsis no longer swallowed by the duplicate-emission filter.claims改为结构化比较。 嵌套的 Map / List 参与相等性与哈希比较,藏在claims内部的变化不再被去重逻辑吞掉。
- An already-due proactive renewal is throttled by
autoRefreshMinInterval(default 5s), so a backend handing out very short-lived tokens cannot turn renewal into a tight loop.- 「已到期」的主动续期现在有节流(
autoRefreshMinInterval,默认 5 秒),后端持续 发放极短寿命的令牌时不会把续期变成紧密循环。
- 「已到期」的主动续期现在有节流(
- Concurrent
restore()calls share one attempt, instead of racing to load and activate two sessions.- 并发的
restore()共享同一次尝试,不再争抢读取并激活两个会话。
- 并发的
- Dropping an unrenewable session completes even when the store fails: the
sign-out happens first and the store error is reported afterwards.
- 丢弃无法续期的会话在存储出错时也能完成:先完成登出,再上报存储错误。
- A throwing
onStateChangedobserver can no longer corrupt the state machine. The observer is a side channel, so its error is swallowed after the state has already been emitted to the stream.- 抛异常的
onStateChanged观察者再也不会破坏状态机。 观察者只是旁路,状态已发 到流上之后,它的异常会被吞掉。
- 抛异常的
AuthManagerGroupforgets a manager disposed outside the group, instead of silently keeping a released manager as the active one.AuthManagerGroup会遗忘在分组之外被释放的管理器,不再把已释放的管理器悄悄 当作激活账号。
Changed / 变更 #
- BREAKING:
AuthTokenSourcegainedvalidAccessToken({Duration? leeway}). Implementations that only declareaccessTokenmust add one line:@override Future<String?> validAccessToken({Duration? leeway}) async => accessToken;.AuthManager(andAuthManagerGroup) override it with a renewing version, so network layers can now guarantee an unexpired token through the interface alone.- 破坏性:
AuthTokenSource新增validAccessToken({Duration? leeway})。 只声明accessToken的实现需补一行:@override Future<String?> validAccessToken({Duration? leeway}) async => accessToken;。AuthManager(与AuthManagerGroup)会覆写为会续期的版本,因此网络层仅凭接口就能 拿到保证未过期的令牌。
- 破坏性:
clockSkewdefaults to 30 seconds. Tokens are treated as expired that much earlier, so a device clock running ahead (or a slow request) cannot hand out a token that dies in flight. PassDuration.zerofor the old behaviour.clockSkew默认为 30 秒。 令牌会提前这么多被视为过期,避免设备时钟偏快(或请求 较慢)时发出一个途中失效的令牌。传Duration.zero可恢复旧行为。
- A single-flight refresh is only joined within the same epoch. Concurrent
callers still share one backend call, but never one that started before the
session was replaced.
- 单飞刷新只在同一 epoch 内被共享。 并发调用方仍共享同一次后端调用,但不会共享 会话被替换之前启动的那一次。
logout()invalidates in-flight work before calling the backend, so a renewal that lands mid-logout can no longer flashAuthenticated.logout()在调用后端之前就让进行中的工作失效,登出途中落地的续期不会再闪一下Authenticated。
Added / 新增 #
clockSkew— how much earlier a token counts as expired (default 30s), plusvalidAccessToken(leeway:)for a per-call override.clockSkew—— 提前多久把令牌视为过期(默认 30 秒);另有validAccessToken(leeway:)供单次调用覆盖。
autoRefreshMinInterval— floor for a proactive renewal that is already due (default 5s).autoRefreshMinInterval—— 「已到期」主动续期的最小等待(默认 5 秒)。
preserveSessionDetails— carry identity fields across a renewal that only returns tokens (defaulttrue; setfalseto keep the backend answer verbatim).preserveSessionDetails—— 在只返回令牌的续期中保留身份字段(默认true; 设为false则原样使用后端响应)。
AuthSession.tryFromJson— deserialize without throwing on malformed input; returnsnullinstead. Use it for anything read from disk or secure storage.AuthSession.tryFromJson—— 反序列化畸形数据时不抛异常,而是返回null。 凡是从磁盘或安全存储读出的内容都建议用它。
AuthState.session— the session carried byAuthenticated/Refreshing/LoggingOut, ornull; no pattern-matching needed for the common case.AuthState.session——Authenticated/Refreshing/LoggingOut携带的 会话(无则为null),常见场景无需再做模式匹配。
AuthManagerGroup:managerFactory, full forwarding of the manager knobs (autoRefreshAhead,clockSkew,refreshFailurePolicy,clock…), an account-taggedonStateChanged,activeIdChanges(a stream of the active account id),validAccessToken()and arestoreAll()that keeps going when one account fails.AuthManagerGroup:新增managerFactory、完整转发管理器调参(autoRefreshAhead、clockSkew、refreshFailurePolicy、clock……)、带账号标记的onStateChanged、activeIdChanges(激活账号 id 流)、validAccessToken(),以及某个账号失败也会 继续的restoreAll()。
Stability / 稳定性 #
- The public surface is now frozen under semver:
AuthStatestays sealed with its current six subtypes,AuthStrategystays at four methods, and new capabilities arrive as additive symbols (or optional interfaces detected withsupports<T>()).- 公共 API 自此按 semver 冻结:
AuthState保持当前六个子类的密封层级,AuthStrategy保持四个方法,新能力以追加符号(或可用supports<T>()检测的可选接口)的形式提供。
- 公共 API 自此按 semver 冻结:
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两条路径。
- 可运行示例与演示后端——