diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md
index 64a688e3..9aca5c60 100644
--- a/.hermes/shared-memory/pitfalls.md
+++ b/.hermes/shared-memory/pitfalls.md
@@ -118,6 +118,22 @@
- 验证:`http://127.0.0.1:3000/api/auth/login-options` 返回至少 `{"availableLoginMethods":["phone","password"]}` 后,登录弹窗会恢复短信登录页签和“获取验证码”按钮。
- 关联:`scripts/api-server-dev.mjs`、`scripts/api-server-maincloud.mjs`、`scripts/dev-rust-stack.sh`、`scripts/dev-web-rust.mjs`、`docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md`。
+## 手机验证码登录 500 先查短信 provider 语义
+
+- 现象:登录弹窗手机号验证码登录失败,浏览器看到 `POST /api/auth/phone/login 500`,后端日志里同时出现阿里云短信 `UNKNOWN`、`biz.FREQUENCY` 或 `check frequency failed`。
+- 原因:真实短信 provider 的配置错误或上游失败曾被 `module-auth` 折叠成 `PhoneAuthError::Store`,HTTP 层只能按内部错误返回 `500`,掩盖了 provider 失败。
+- 处理:保留 provider 错误语义,配置错误映射 `503 Service Unavailable`,上游短信失败映射 `502 Bad Gateway`;本地只验证 UI/账号链路时可用 shell 临时覆盖 `SMS_AUTH_PROVIDER=mock` 后启动 `npm run api-server`。
+- 验证:`cargo test -p api-server phone_auth_sms_provider_errors_keep_upstream_http_semantics --manifest-path server-rs/Cargo.toml`,真实 provider 频控时接口不再返回 `500`。
+- 关联:`server-rs/crates/module-auth/src/errors.rs`、`server-rs/crates/api-server/src/phone_auth.rs`、`docs/technical/PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md`。
+
+## 手机验证码登录成功后又瞬间回到未登录
+
+- 现象:手机号验证码登录先成功,随后 UI 又闪回“未登录”,登录弹窗可能重新出现。
+- 原因:`AuthGate` 首次 hydrate 会异步轮换 refresh cookie 并请求 `/api/auth/me`。如果用户在 hydrate 完成前已经登录,晚到的旧 hydrate 仍可能把刚写入的 `user` 覆盖成 `null`。
+- 处理:给 `AuthGate` 的 hydrate 增加版本号保护;登录成功、退出登录和全局 auth 事件都会推进版本号,旧 hydrate 结果到达后直接丢弃。
+- 验证:`npm run test -- src/components/auth/AuthGate.test.tsx`,新增用例应覆盖“旧 guest hydrate 不覆盖新登录态”。
+- 关联:`src/components/auth/AuthGate.tsx`、`src/components/auth/AuthGate.test.tsx`、`docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md`。
+
## Rust 冷编译导致 api-server 健康检查误超时
- 现象:`npm run dev:rust` 在 Windows 冷编译/链接阶段误判 `/healthz` 等待超时并杀掉 `cargo run`。
diff --git a/docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md b/docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md
new file mode 100644
index 00000000..04447b4a
--- /dev/null
+++ b/docs/technical/AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md
@@ -0,0 +1,37 @@
+# `AuthGate` 登录后又回到未登录状态修复
+
+日期:`2026-05-09`
+
+## 背景
+
+本地联调中,手机号验证码登录有时会先显示登录成功,随后又瞬间回到未登录态。
+
+## 根因
+
+`AuthGate` 首次挂载时会异步 hydrate:
+
+1. 先轮换 refresh cookie
+2. 再请求 `/api/auth/me`
+3. 再根据结果写入 `user` 和 `status`
+
+如果用户在这轮 hydrate 尚未完成时已经完成了登录,后到达的旧 hydrate 结果仍可能把刚写入的 `user` 覆盖回 `null`,导致登录态闪回未登录。
+
+## 修复
+
+`AuthGate` 增加 hydrate 版本号保护:
+
+1. 每次启动 hydrate 都分配独立版本号。
+2. 登录成功、退出登录、收到全局 auth state 事件时递增版本号。
+3. 旧版本 hydrate 的结果到达后直接丢弃,不再覆盖当前 `user` / `status`。
+
+## 验证
+
+1. `npm run test -- src/components/auth/AuthGate.test.tsx`
+2. `npm run test -- src/services/apiClient.test.ts src/services/authService.test.ts`
+3. `npm run check:encoding`
+
+## 关联
+
+- `src/components/auth/AuthGate.tsx`
+- `src/components/auth/AuthGate.test.tsx`
+- `.hermes/shared-memory/pitfalls.md`
diff --git a/docs/technical/PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md b/docs/technical/PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md
new file mode 100644
index 00000000..1fe10c07
--- /dev/null
+++ b/docs/technical/PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md
@@ -0,0 +1,66 @@
+# 手机验证码短信 Provider 错误 HTTP 映射修复
+
+日期:`2026-05-08`
+
+## 背景
+
+本地登录弹窗点击手机号验证码登录时,浏览器报:
+
+```text
+POST /api/auth/phone/login 500
+```
+
+排查发现当前 `.env.local` 使用:
+
+```text
+SMS_AUTH_PROVIDER=aliyun
+```
+
+因此 `send-code` 会走真实阿里云短信 provider。真实 provider 返回 `UNKNOWN` 或 `biz.FREQUENCY / check frequency failed` 时,`module-auth` 曾把 provider 失败统一折叠成 `PhoneAuthError::Store`,`api-server` 再映射为 `500 Internal Server Error`,前端只能看到登录失败。
+
+## 根因
+
+短信 provider 失败不是认证仓储内部错误:
+
+1. 阿里云配置缺失或配置非法属于服务配置问题。
+2. 阿里云返回频控、网关失败或业务失败属于上游短信 provider 问题。
+3. 这些错误不应被映射成 `Store`,否则 HTTP 层无法区分真实内部错误与外部 provider 失败。
+
+## 修复
+
+`module-auth` 新增短信 provider 错误分类:
+
+1. `PhoneAuthError::SmsProviderInvalidConfig`
+2. `PhoneAuthError::SmsProviderUpstream`
+
+`api-server` 映射规则调整为:
+
+1. provider 配置错误返回 `503 Service Unavailable`
+2. provider 上游失败返回 `502 Bad Gateway`
+3. 验证码不存在、错误、过期仍返回 `400`
+4. 本地仓储或签发错误仍返回 `500`
+
+## 本地排查
+
+如果本地只想验证登录 UI 和账号链路,可以临时用 shell 环境覆盖真实短信 provider:
+
+```powershell
+$env:SMS_AUTH_PROVIDER="mock"
+npm run api-server
+```
+
+若要验证真实短信链路,保持 `SMS_AUTH_PROVIDER=aliyun`,并查看 `api-server` 日志中的:
+
+1. `阿里云短信发送接口返回响应`
+2. `阿里云短信发送接口返回业务失败`
+3. `手机号验证码发送失败`
+
+看到 `biz.FREQUENCY` / `check frequency failed` 时,说明请求已到达短信 provider,但被 provider 频控或业务规则拒绝。
+
+## 验收
+
+1. `cargo test -p api-server phone_auth_sms_provider_errors_keep_upstream_http_semantics --manifest-path server-rs/Cargo.toml`
+2. `cargo test -p api-server send_phone --manifest-path server-rs/Cargo.toml`
+3. `cargo test -p api-server phone_login_creates_user_and_sets_refresh_cookie --manifest-path server-rs/Cargo.toml`
+4. `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
+5. `npm run check:encoding`
diff --git a/docs/technical/README.md b/docs/technical/README.md
index 3954b8ed..b8a8ab98 100644
--- a/docs/technical/README.md
+++ b/docs/technical/README.md
@@ -5,6 +5,7 @@
## 文档列表
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异。
+- [AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md](./AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md):记录 `AuthGate` 登录成功后又被旧 hydrate 覆盖回未登录态的竞态根因、版本号保护修复与回归测试。
- [VOLCENGINE_SPEECH_STREAMING_INTEGRATION_2026-05-08.md](./VOLCENGINE_SPEECH_STREAMING_INTEGRATION_2026-05-08.md):记录火山引擎大模型 ASR 双向流式、TTS WebSocket 双向流式和 TTS HTTP SSE 单向流式的后端代理、环境变量、协议帧和验收边界。
- [VECTOR_ENGINE_AUDIO_GENERATION_SUNO_VIDU_2026-05-08.md](./VECTOR_ENGINE_AUDIO_GENERATION_SUNO_VIDU_2026-05-08.md):记录视觉小说结果页接入 VectorEngine Suno 文生背景音乐与 Vidu 文生音效的接口、环境变量、后端路由、OSS 资产回写和前端弹层交互边界。
- [PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md](./PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md):冻结“我的”页签帮助与反馈入口的后端接入方案,覆盖 `POST /api/profile/feedback`、`profile_feedback_submission`、凭证图片 Data URL 校验和前端预览/提交边界。
@@ -163,6 +164,7 @@
- [PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md](./PHONE_AUTH_AXUM_REAL_SMS_PROVIDER_DESIGN_2026-04-22.md):冻结 Rust `api-server + module-auth + platform-auth` 接入真实阿里云短信 provider 的 crate 边界、发送与校验职责、配置项和错误语义。
- [PHONE_SMS_ALIYUN_RESPONSE_FIELD_MAPPING_FIX_2026-04-23.md](./PHONE_SMS_ALIYUN_RESPONSE_FIELD_MAPPING_FIX_2026-04-23.md):记录 Rust `platform-auth` 把阿里云 PascalCase 响应字段误判成空值的问题根因,并冻结字段映射修复与回归标准。
- [PHONE_SMS_SEND_CODE_OBSERVABILITY_FIX_2026-04-23.md](./PHONE_SMS_SEND_CODE_OBSERVABILITY_FIX_2026-04-23.md):冻结手机号验证码发送链路的日志补强口径,确保 `api-server`、`module-auth`、`platform-auth` 能直接暴露发送前后与错误分类关键字段。
+- [PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md](./PHONE_SMS_PROVIDER_ERROR_HTTP_MAPPING_FIX_2026-05-08.md):记录真实短信 provider 返回 `UNKNOWN` / `biz.FREQUENCY` 时被误映射成登录 `500` 的根因,冻结 provider 配置错误 `503`、上游失败 `502` 的 HTTP 映射。
- [PHONE_SMS_DELIVERY_OBSERVABILITY_AND_RECEIPT_DESIGN_2026-04-22.md](./PHONE_SMS_DELIVERY_OBSERVABILITY_AND_RECEIPT_DESIGN_2026-04-22.md):冻结短信平台受理成功与最终送达状态的区分方式、追踪字段、送达回执接口和前端提示文案边界。
- [PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md](./PHONE_SMS_REAL_PROVIDER_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md):冻结验证清单第一项“真实短信验证码链路”的本地启动、前端操作、日志观察点、通过标准与失败排查步骤。
- [ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md](./ASSET_EXTERNAL_GENERATION_MANUAL_VERIFICATION_RUNBOOK_2026-04-23.md):冻结验证清单第四项“图片、视频、动作真实外部生成”的人工联调口径,明确哪些入口已接真实外部图片服务、哪些入口仍是 Stage 1 占位链,以及前端点击路径、日志观察点和通过标准。
diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs
index 37e11519..0170c726 100644
--- a/server-rs/crates/api-server/src/app.rs
+++ b/server-rs/crates/api-server/src/app.rs
@@ -3269,6 +3269,31 @@ mod tests {
assert_eq!(login_response.status(), StatusCode::OK);
}
+ #[test]
+ fn phone_auth_sms_provider_errors_keep_upstream_http_semantics() {
+ let invalid_config = crate::phone_auth::map_phone_auth_error(
+ module_auth::PhoneAuthError::SmsProviderInvalidConfig(
+ "阿里云短信 AccessKeyId 未配置".to_string(),
+ ),
+ );
+ assert_eq!(
+ invalid_config.status_code(),
+ StatusCode::SERVICE_UNAVAILABLE
+ );
+ assert_eq!(invalid_config.message(), "阿里云短信 AccessKeyId 未配置");
+
+ let upstream = crate::phone_auth::map_phone_auth_error(
+ module_auth::PhoneAuthError::SmsProviderUpstream(
+ "短信验证码发送失败:check frequency failed".to_string(),
+ ),
+ );
+ assert_eq!(upstream.status_code(), StatusCode::BAD_GATEWAY);
+ assert_eq!(
+ upstream.message(),
+ "短信验证码发送失败:check frequency failed"
+ );
+ }
+
#[tokio::test]
async fn wechat_start_returns_mock_callback_url_with_state() {
let config = AppConfig {
diff --git a/server-rs/crates/api-server/src/phone_auth.rs b/server-rs/crates/api-server/src/phone_auth.rs
index 16b2ed5d..31e64afa 100644
--- a/server-rs/crates/api-server/src/phone_auth.rs
+++ b/server-rs/crates/api-server/src/phone_auth.rs
@@ -320,6 +320,12 @@ pub fn map_phone_auth_error(error: PhoneAuthError) -> AppError {
PhoneAuthError::UserNotFound => {
AppError::from_status(StatusCode::UNAUTHORIZED).with_message(error.to_string())
}
+ PhoneAuthError::SmsProviderInvalidConfig(_) => {
+ AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message(error.to_string())
+ }
+ PhoneAuthError::SmsProviderUpstream(_) => {
+ AppError::from_status(StatusCode::BAD_GATEWAY).with_message(error.to_string())
+ }
PhoneAuthError::Store(_) | PhoneAuthError::PasswordHash(_) => {
map_phone_auth_platform_store_error(error.to_string())
}
diff --git a/server-rs/crates/api-server/src/wechat_auth.rs b/server-rs/crates/api-server/src/wechat_auth.rs
index be1b6faa..2584e504 100644
--- a/server-rs/crates/api-server/src/wechat_auth.rs
+++ b/server-rs/crates/api-server/src/wechat_auth.rs
@@ -396,6 +396,12 @@ fn map_wechat_bind_phone_error(error: module_auth::PhoneAuthError) -> AppError {
module_auth::PhoneAuthError::UserNotFound => {
AppError::from_status(StatusCode::UNAUTHORIZED).with_message(error.to_string())
}
+ module_auth::PhoneAuthError::SmsProviderInvalidConfig(_) => {
+ AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message(error.to_string())
+ }
+ module_auth::PhoneAuthError::SmsProviderUpstream(_) => {
+ AppError::from_status(StatusCode::BAD_GATEWAY).with_message(error.to_string())
+ }
module_auth::PhoneAuthError::Store(_) | module_auth::PhoneAuthError::PasswordHash(_) => {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
}
diff --git a/server-rs/crates/module-auth/src/errors.rs b/server-rs/crates/module-auth/src/errors.rs
index 22c39527..9117ab6e 100644
--- a/server-rs/crates/module-auth/src/errors.rs
+++ b/server-rs/crates/module-auth/src/errors.rs
@@ -28,6 +28,8 @@ pub enum PhoneAuthError {
VerifyAttemptsExceeded,
UserNotFound,
UserStateMismatch,
+ SmsProviderInvalidConfig(String),
+ SmsProviderUpstream(String),
Store(String),
PasswordHash(String),
}
@@ -88,6 +90,9 @@ impl fmt::Display for PhoneAuthError {
Self::VerifyAttemptsExceeded => f.write_str("验证码错误次数过多,请重新获取验证码"),
Self::UserNotFound => f.write_str("用户不存在"),
Self::UserStateMismatch => f.write_str("当前账号状态不允许执行该操作"),
+ Self::SmsProviderInvalidConfig(message) | Self::SmsProviderUpstream(message) => {
+ f.write_str(message)
+ }
Self::Store(message) | Self::PasswordHash(message) => f.write_str(message),
}
}
diff --git a/server-rs/crates/module-auth/src/lib.rs b/server-rs/crates/module-auth/src/lib.rs
index 60f0e9fe..78bbd7ef 100644
--- a/server-rs/crates/module-auth/src/lib.rs
+++ b/server-rs/crates/module-auth/src/lib.rs
@@ -1862,9 +1862,10 @@ impl InMemoryAuthStore {
fn map_sms_provider_error_to_phone_error(error: SmsProviderError) -> PhoneAuthError {
match error {
SmsProviderError::InvalidVerifyCode => PhoneAuthError::InvalidVerifyCode,
- SmsProviderError::InvalidConfig(message) | SmsProviderError::Upstream(message) => {
- PhoneAuthError::Store(message)
+ SmsProviderError::InvalidConfig(message) => {
+ PhoneAuthError::SmsProviderInvalidConfig(message)
}
+ SmsProviderError::Upstream(message) => PhoneAuthError::SmsProviderUpstream(message),
}
}
diff --git a/src/components/auth/AuthGate.test.tsx b/src/components/auth/AuthGate.test.tsx
index 42c4411e..e5ae9b62 100644
--- a/src/components/auth/AuthGate.test.tsx
+++ b/src/components/auth/AuthGate.test.tsx
@@ -330,6 +330,42 @@ test('auth gate opens a login modal for protected actions and resumes after logi
expect(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();
});
+test('phone login result is not overwritten by an older guest hydrate', async () => {
+ const user = userEvent.setup();
+ const onAuthenticated = vi.fn();
+ authMocks.getAuthLoginOptions.mockResolvedValue({
+ availableLoginMethods: ['phone'],
+ });
+ authMocks.getCurrentAuthUser
+ .mockResolvedValueOnce({
+ user: null,
+ availableLoginMethods: ['phone'],
+ })
+ .mockResolvedValue({
+ user: mockUser,
+ availableLoginMethods: ['phone'],
+ });
+
+ render(
+
+
+
+ ,
+ );
+
+ await user.click(await screen.findByRole('button', { name: '进入作品' }));
+ const dialog = screen.getByRole('dialog', { name: '账号入口' });
+ await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
+ await user.type(within(dialog).getByLabelText('验证码'), '123456');
+ await user.click(within(dialog).getByRole('button', { name: '登录' }));
+
+ expect(await screen.findByText('当前用户:测试玩家')).toBeTruthy();
+ expect(onAuthenticated).toHaveBeenCalledTimes(1);
+
+ expect(screen.getByText('当前用户:测试玩家')).toBeTruthy();
+ expect(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();
+});
+
test('auth gate hides register entry and opens invite modal for new sms account', async () => {
const user = userEvent.setup();
window.history.replaceState(null, '', '/?inviteCode=spring-2026');
diff --git a/src/components/auth/AuthGate.tsx b/src/components/auth/AuthGate.tsx
index a853a157..b009dfa1 100644
--- a/src/components/auth/AuthGate.tsx
+++ b/src/components/auth/AuthGate.tsx
@@ -120,6 +120,7 @@ export function AuthGate({ children }: AuthGateProps) {
const pendingProtectedActionRef = useRef<(() => void) | null>(null);
const autoOpenedInviteCodeRef = useRef(null);
const hasRenderedPlatformContentRef = useRef(false);
+ const authHydrateVersionRef = useRef(0);
const canKeepPlatformContentMounted =
hasRenderedPlatformContentRef.current &&
(status === 'checking' || status === 'recovering');
@@ -134,6 +135,7 @@ export function AuthGate({ children }: AuthGateProps) {
const activateReadyUser = useCallback((nextUser: AuthUser) => {
// 受保护业务 hook 只在 readyUser 暴露后启动,必须先保证请求层能带 Bearer token。
+ authHydrateVersionRef.current += 1;
setUser(nextUser);
setStatus('ready');
}, []);
@@ -141,6 +143,7 @@ export function AuthGate({ children }: AuthGateProps) {
const clearLocalAuthenticatedState = useCallback(() => {
// 退出动作必须先收回前端鉴权上下文,再等待后端吊销完成。
// 否则平台壳层会在无刷新状态下继续暴露旧用户的私有作品缓存。
+ authHydrateVersionRef.current += 1;
pendingProtectedActionRef.current = null;
setUser(null);
setStatus('unauthenticated');
@@ -268,11 +271,13 @@ export function AuthGate({ children }: AuthGateProps) {
useEffect(() => {
let isActive = true;
- const hydrate = async () => {
+ const hydrate = async (hydrateToken: number) => {
+ const isCurrentHydrate = () =>
+ isActive && hydrateToken === authHydrateVersionRef.current;
const callbackResult = consumeAuthCallbackResult();
const loadLoginOptions = async () => {
const options = await getAuthLoginOptions();
- if (!isActive) {
+ if (!isCurrentHydrate()) {
return null;
}
@@ -285,14 +290,14 @@ export function AuthGate({ children }: AuthGateProps) {
const resolveGuestFallback = async () => {
try {
await loadLoginOptions();
- if (!isActive) {
+ if (!isCurrentHydrate()) {
return;
}
setUser(null);
setStatus('unauthenticated');
} catch (optionsError) {
- if (!isActive) {
+ if (!isCurrentHydrate()) {
return;
}
@@ -305,7 +310,7 @@ export function AuthGate({ children }: AuthGateProps) {
}
};
- if (callbackResult?.error && isActive) {
+ if (callbackResult?.error && isCurrentHydrate()) {
setError(callbackResult.error);
setShowLoginModal(true);
}
@@ -315,8 +320,11 @@ export function AuthGate({ children }: AuthGateProps) {
// 后端只在 refresh/session 成功续期时写每日登录埋点;如果本地 access token 尚未过期,
// 仅调用 /auth/me 不会进入续期链路,导致“打开网页”没有登录埋点。
await refreshStoredAccessToken();
+ if (!isCurrentHydrate()) {
+ return;
+ }
const nextSession = await getCurrentAuthUser();
- if (!isActive) {
+ if (!isCurrentHydrate()) {
return;
}
@@ -339,7 +347,7 @@ export function AuthGate({ children }: AuthGateProps) {
);
setError(callbackResult?.error ?? '');
} catch {
- if (!isActive) {
+ if (!isCurrentHydrate()) {
return;
}
@@ -347,11 +355,11 @@ export function AuthGate({ children }: AuthGateProps) {
}
};
- void hydrate();
+ void hydrate(++authHydrateVersionRef.current);
const handleAuthStateChange = () => {
setStatus('checking');
- void hydrate();
+ void hydrate(++authHydrateVersionRef.current);
};
window.addEventListener(AUTH_STATE_EVENT, handleAuthStateChange);