Skip to content

feat(frontend): Complete account authentication loop and unified session invalidation - #218

Merged
Wintercom merged 8 commits into
1024XEngineer:mainfrom
Alexander-Noah:feat/frontend-auth-session-store
Aug 13, 2026
Merged

feat(frontend): Complete account authentication loop and unified session invalidation#218
Wintercom merged 8 commits into
1024XEngineer:mainfrom
Alexander-Noah:feat/frontend-auth-session-store

Conversation

@Alexander-Noah

Copy link
Copy Markdown
Contributor

关联 Issue

Refs #194

变更概述

完成前端账户认证闭环,包括安全会话存储、认证状态恢复、登录状态路由切换、HTTP Bearer Token 注入、WebSocket 会话认证、统一失效清理、账号内存清理及相关自动化测试。

认证页面和业务展示层只消费不含 Token 的 AuthViewState;HTTP、WebSocket 和主动退出共用同一个失效协调器,避免重复实现认证清理逻辑。

依赖与测试配置

frontend/package.json

  • 增加 expo-secure-store,用于 Android/iOS 安全保存认证会话。
  • 保留 Expo Web、Vitest、Jest 和 React Native Testing Library 配置。
  • 分离 Vitest、Jest 单元测试和 Jest 集成测试的收集范围,避免重复运行。
  • 增加 verify:ci,组合代码检查和 Android Expo export。

frontend/package-lock.json

  • 同步前端认证及测试依赖的锁定版本。
  • 文件由 npm 自动维护,不包含手写业务逻辑。

frontend/.env.example

  • 增加 WebSocket URL 和固定设备 ID 配置示例。
  • Android 模拟器默认通过 10.0.2.2 访问宿主机服务。

认证契约与领域模型

frontend/src/contracts/auth.ts

  • 定义统一登录或创建账号的请求、响应和错误契约。
  • 成功响应严格要求 account_id、非空 opaque access_tokenexpires_in=3600
  • 使用唯一的 parseAuthErrorEnvelope() 解析冻结的嵌套错误外壳。
  • 统一业务错误、无效响应、网络错误和超时错误语义。

frontend/src/features/auth/domain/authSession.ts

  • 定义内部 AuthSession、认证状态和无 Token 的展示状态。
  • 集中计算 Token 到期时间和明显过期判断。
  • Token 保持 opaque string,不解析 JWT。

安全会话存储

frontend/src/features/auth/data/MemoryAuthSessionStore.ts

  • Web 平台仅在当前进程内保存认证会话。
  • 不使用 localStorage、sessionStorage 或 IndexedDB。

frontend/src/features/auth/data/SecureAuthSessionStore.ts

  • Android/iOS 使用 SecureStore 保存认证会话。
  • 严格编解码会话记录并清理损坏或过期数据。
  • 存储错误只暴露固定脱敏错误语义。

frontend/src/features/auth/application/AuthSessionDeletionRetrier.ts

  • 会话删除失败时调度后台重试。
  • 新登录等待旧删除结束,避免旧清理误删新会话。
  • 使用代次隔离并发删除和新认证写入。

认证状态与路由闭环

frontend/src/features/auth/application/AuthController.ts

  • 作为认证内部状态的唯一写入者。
  • 支持启动恢复、初始化重试、认证成功、主动退出和失效清理。
  • 只有持久化成功后才发布 authenticated 状态。
  • 清理失败时仍优先发布 unauthenticated,并由 retrier 继续删除。

frontend/src/features/auth/presentation/AuthProvider.tsx

  • React Context 只暴露无 Token 的 AuthViewState 和认证动作。
  • 页面和业务 ViewModel 无法读取内部 session 或访问 Token。
  • 主动退出通过统一失效协调器执行。

frontend/src/app/AppRoot.tsx

  • 根据 loading、unauthenticated 和 authenticated 状态切换根页面。
  • 支持认证恢复失败后的重试入口。
  • 登录成功页面只展示账号 ID,不展示或输出 Token。

HTTP Token 与统一失效

frontend/src/infrastructure/network/client.ts

  • 公开请求强制删除 Authorization,包括调用方传入的旧认证头。
  • 受保护请求自动添加 Bearer <opaque-token>
  • 无 Token 或正在失效时返回固定本地未认证错误。
  • 仅 protected + HTTP 401 + AUTH_REQUIRED/AUTH_INVALID_TOKEN 触发失效。
  • 未知 401、网络错误和其他 5xx 保留当前会话。
  • 保留所有非认证请求头。

frontend/src/features/auth/application/AuthInvalidationCoordinator.ts

  • HTTP、WebSocket 和主动退出共用唯一失效协调器。
  • 并发失效请求复用同一个清理 Promise。
  • 固定执行 socket → account state → session/controller 清理顺序。
  • 任一阶段失败不会阻断后续清理。

WebSocket 会话认证

frontend/src/contracts/authWebSocket.ts

  • 定义 session.hellosession.readysession.error 冻结契约。
  • 集中创建认证首帧并解析服务端认证消息。
  • 严格校验 request_id、错误码、响应结构和带时区的 server_time。
  • 拒绝非法 JSON、未知消息和原型链字段。

frontend/src/infrastructure/websocket/AuthenticatedWebSocketClient.ts

  • 实现 disconnected | connecting | authenticating | ready 状态机。
  • 获取 Token 前后检查统一失效状态。
  • Token 等待期间发生退出或失效时不创建 socket。
  • URL 和 hello payload 使用同一个非空 device_id。
  • ready 前拒绝 JSON 和 Binary 业务帧,不排队。
  • 只接受 request_id 匹配的 session.ready
  • UNAUTHENTICATED 触发统一失效。
  • 协议错误、超时、普通断线和 1013 只关闭当前连接。
  • 不实现自动重连。
  • connect()close() 支持幂等调用。

账号状态清理与脱敏诊断

frontend/src/features/auth/application/AccountStateCleanerRegistry.ts

  • 只允许注册 schedule-viewreminder-runtime 两个固定账号状态来源。
  • 拒绝重复 key。
  • 按注册顺序执行清理。
  • 单个 cleaner 失败不阻断后续清理。

frontend/src/features/auth/application/AuthDiagnostics.ts

  • 诊断事件只包含固定 event 和固定 component。
  • 不接受账号 ID、Token、密码、传输帧或原始异常。
  • coordinator、账号清理和删除重试复用同一个诊断入口。

frontend/src/app/authRuntime.ts

  • 统一组合 Controller、SecureStore、删除 retrier、诊断、账号清理注册表、HTTP client 和 WebSocket client。
  • HTTP、WebSocket 和主动退出共享同一个 coordinator。
  • 支持测试注入固定时钟、存储、fetch 和 socket factory。

固定夹具与 Fake Transport

frontend/tests/fixtures/auth/

  • 增加认证成功、错误凭据和无效 Token HTTP 夹具。
  • 增加 WebSocket hello、ready、unauthenticated 和 malformed 夹具。
  • 固定 expires_in=3600
  • 固定 server_time=2026-08-06T03:00:00+00:00

frontend/tests/fakes/FakeAuthHttpTransport.ts

  • 提供唯一、确定性的 HTTP Fake。
  • 支持固定 JSON、延迟响应和网络错误。
  • 不输出请求内容或敏感凭据。

frontend/src/infrastructure/websocket/testing/FakeWebSocket.ts

  • 提供唯一、确定性的 WebSocket Fake。
  • 显式驱动 open、message、error 和 close 事件。

自动化测试

frontend/tests/contracts/authContract.test.ts

  • 验证公共 parser、builder 与冻结 HTTP/WS 夹具一致。
  • 固定 Token 有效期和 WebSocket server_time。

frontend/tests/integration/authClosure.integration.test.ts

覆盖以下完整账户闭环:

  • 新账号和已有账号复用同一认证入口。
  • 错误凭据不写 store、不改变根认证路由。
  • 同一 opaque Token 用于受保护 HTTP 和 WebSocket hello。
  • 公开请求不携带 Authorization。
  • 受保护请求保留非认证请求头。
  • 并发 HTTP 401 和 WS UNAUTHENTICATED 只执行一次清理。
  • HTTP 500、网络错误、WS 1013 和 MALFORMED_MESSAGE 保留会话。
  • SecureStore 删除失败时先返回登录状态,后台重试成功后删除旧记录。
  • 旧删除完成前新 session 不写入。
  • 主动退出关闭 socket、清理账号内存、删除 session 并返回登录状态。

变更原因

Issue #194 需要建立完整、低冗余且安全的前端账户认证闭环。

本 PR 将认证契约、领域状态、持久化、HTTP、WebSocket、页面展示和失效清理拆分为明确边界,并确保:

  • Token 不进入展示层。
  • Token 不被持久化到 Web 存储。
  • 同一协议解析、过期判断和清理编排只有一个权威实现。
  • 并发认证失效不会重复清理。
  • 失败诊断不泄露敏感数据。
  • 前端账户系统可以在不依赖真实后端分支的情况下完成自动化验收。

破坏性变更

无已知破坏性变更。

验证结果

  • ESLint 检查通过。
  • TypeScript 类型检查通过。
  • Vitest:5 个测试文件、34 个测试用例通过。
  • Jest:19 个测试套件、158 个测试用例通过。
  • Android Expo export 通过。
  • HTTP/WS 冻结契约测试通过。
  • 认证闭环集成测试通过。
  • 并发失效和异步删除竞态测试通过。
  • Token 展示层边界扫描通过。
  • Web 持久化、Refresh Token、JWT 解析和敏感日志扫描通过。
  • 核心 parser、coordinator、retrier、Fake WebSocket 和 Fake HTTP Transport 单一实现检查通过。
  • 所有提交文件均位于 frontend/,未修改后端代码。
  • Prettier 未纳入本次本地验收门禁,按当前约定忽略既有格式基线问题。

本次不包含

  • 真实前后端登录或 WebSocket 联调。
  • Refresh Token、Token 撤销和自动续期。
  • Web 持久会话或 HttpOnly Cookie 流程。
  • WebSocket 自动重连和离线消息排队。
  • 密码找回、改密、验证码、第三方登录或生物识别。
  • 服务端会话和多设备管理。
  • 后端接口或后端测试修改。

@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
timeflow Ready Ready Preview Aug 13, 2026 1:49am

@Alexander-Noah

Copy link
Copy Markdown
Contributor Author

@fennoai /review

@fennoai

fennoai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review complete

Submitted one non-blocking PR review with 3 inline findings:

  • 2 high-severity production composition gaps: protected transports are not connected to app services, and account-state cleaners are never registered.
  • 1 medium-severity WebSocket gap: business frames are discarded after authentication because no receive path is exposed.
  • git diff --check passed.
  • TypeScript/Jest were not run because frontend/node_modules is absent in the mounted workspace.

View job run

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Found three production integration gaps in the new authentication runtime. The isolated controller/client tests are extensive, but the app composition currently leaves protected transports and account-state cleanup disconnected from the existing application services.

Validation note: git diff --check passed. I could not execute TypeScript/Jest locally because frontend/node_modules is not installed in the mounted workspace.

Comment thread frontend/src/app/AppRoot.tsx Outdated
Comment thread frontend/src/app/authRuntime.ts

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found in this review. I reviewed the pinned diff through code quality, performance, security, and documentation passes. The previously reported production composition, account-state cleanup, and post-handshake WebSocket delivery gaps are addressed in 734e085. git diff --check passed; frontend lint/type/tests were not rerun because frontend/node_modules is absent.

View job run

@Wintercom Wintercom left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@Wintercom
Wintercom merged commit 5a5ee87 into 1024XEngineer:main Aug 13, 2026
5 checks passed
gac0812 added a commit to gac0812/Tfgxs that referenced this pull request Aug 13, 2026
…e-alarm

Keep NativeAlarmScheduler wiring while adopting the auth runtime composition from 1024XEngineer#218.
gac0812 added a commit to gac0812/Tfgxs that referenced this pull request Aug 13, 2026
…on-foundation

Keep expo-location wiring while adopting the auth runtime from 1024XEngineer#218.
LUPENGHAN added a commit to LUPENGHAN/timeflow that referenced this pull request Aug 13, 2026
)

- rewrite AppRoot.tsx onto 1024XEngineer#218's AuthController/useAuth() structure;
  AuthenticatedScheduleRoute now renders HomeScreen (voice-enabled)
  instead of ScheduleCalendarScreen directly, sourcing the access token
  via AuthController.getAccessToken() rather than the token-free
  AuthViewState
- drop the username-display feature (its mount points no longer exist
  post-1024XEngineer#218); calendar header shows accountId again
- mock the assistant's native-backed modules in AppRoot's test so the
  suite doesn't try to load a real audio/location native module
- relocate useScheduleCalendar.test.ts / ScheduleCalendarScreen.test.tsx
  from src/ to tests/unit/, matching 1024XEngineer#218's jest testMatch which no
  longer picks up colocated src/**/*.test.ts files — these were
  silently not running after the merge otherwise

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Alexander-Noah
Alexander-Noah deleted the feat/frontend-auth-session-store branch August 13, 2026 07:25
LUPENGHAN added a commit to LUPENGHAN/timeflow that referenced this pull request Aug 13, 2026
…lient

Voice used to run its own session.hello/session.ready handshake over a
private WebSocketVoiceTransport, duplicating what 1024XEngineer#218's
AuthenticatedWebSocketClient already does for the app's one shared
connection. Collapse the two:

- add onClose(listener) to AuthenticatedWebSocketClient — it previously
  had no way to notify callers when a ready connection dropped, which
  voice's "连接已断开" UI relies on; only fires for post-ready drops,
  not handshake failures (those already reject connect())
- thread an optional location through connect() → session.hello, since
  voice needs to attach coordinates to the same first frame
- always include the device's IANA timezone in session.hello — it was
  declared in the schema and read server-side (session.py) but never
  actually sent by any client; the backend that consumes it
  (feature/client-timezone-latlong) isn't merged yet, but this unblocks
  it without further frontend changes once it lands
- replace WebSocketVoiceTransport with AuthenticatedVoiceTransport, a
  thin adapter that demuxes the client's generic string|ArrayBuffer
  channel into the JSON-control/binary-audio split VoiceTransportPort
  expects; it does not close the shared connection itself — that stays
  owned by the auth system (AuthInvalidationCoordinator)
- AssistantConversationService.connect() no longer builds/sends
  session.hello itself; the transport is ready by the time it resolves
- drop the now-dead SessionHelloMessage/SessionHelloPayload/
  SessionReadyMessage from contracts/conversation.ts — that handshake
  lives entirely in contracts/authWebSocket.ts now
- AssistantApplicationOptions shrinks to { accountId } — access_token/
  device_id/wsUrl are owned by the shared client, not voice
- add test coverage for onClose, location-in-hello, and timezone-in-hello

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LUPENGHAN added a commit to LUPENGHAN/timeflow that referenced this pull request Aug 13, 2026
)

- rewrite AppRoot.tsx onto 1024XEngineer#218's AuthController/useAuth() structure;
  AuthenticatedScheduleRoute now renders HomeScreen (voice-enabled)
  instead of ScheduleCalendarScreen directly, sourcing the access token
  via AuthController.getAccessToken() rather than the token-free
  AuthViewState
- drop the username-display feature (its mount points no longer exist
  post-1024XEngineer#218); calendar header shows accountId again
- mock the assistant's native-backed modules in AppRoot's test so the
  suite doesn't try to load a real audio/location native module
- relocate useScheduleCalendar.test.ts / ScheduleCalendarScreen.test.tsx
  from src/ to tests/unit/, matching 1024XEngineer#218's jest testMatch which no
  longer picks up colocated src/**/*.test.ts files — these were
  silently not running after the merge otherwise

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LUPENGHAN added a commit to LUPENGHAN/timeflow that referenced this pull request Aug 13, 2026
…lient

Voice used to run its own session.hello/session.ready handshake over a
private WebSocketVoiceTransport, duplicating what 1024XEngineer#218's
AuthenticatedWebSocketClient already does for the app's one shared
connection. Collapse the two:

- add onClose(listener) to AuthenticatedWebSocketClient — it previously
  had no way to notify callers when a ready connection dropped, which
  voice's "连接已断开" UI relies on; only fires for post-ready drops,
  not handshake failures (those already reject connect())
- thread an optional location through connect() → session.hello, since
  voice needs to attach coordinates to the same first frame
- always include the device's IANA timezone in session.hello — it was
  declared in the schema and read server-side (session.py) but never
  actually sent by any client; the backend that consumes it
  (feature/client-timezone-latlong) isn't merged yet, but this unblocks
  it without further frontend changes once it lands
- replace WebSocketVoiceTransport with AuthenticatedVoiceTransport, a
  thin adapter that demuxes the client's generic string|ArrayBuffer
  channel into the JSON-control/binary-audio split VoiceTransportPort
  expects; it does not close the shared connection itself — that stays
  owned by the auth system (AuthInvalidationCoordinator)
- AssistantConversationService.connect() no longer builds/sends
  session.hello itself; the transport is ready by the time it resolves
- drop the now-dead SessionHelloMessage/SessionHelloPayload/
  SessionReadyMessage from contracts/conversation.ts — that handshake
  lives entirely in contracts/authWebSocket.ts now
- AssistantApplicationOptions shrinks to { accountId } — access_token/
  device_id/wsUrl are owned by the shared client, not voice
- add test coverage for onClose, location-in-hello, and timezone-in-hello

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wintercom pushed a commit that referenced this pull request Aug 13, 2026
…r screen (#229)

* feat(assistant): wire voice WS transport, audio pipeline, and calendar integration

- add VoiceTransportPort/WebSocketVoiceTransport for the session.hello →
  voice.stream.* → voice.command.result WS protocol
- add AudioCapturePort/AudioPlaybackPort backed by @irvingouj/expo-audio-stream
  (patched via patch-package), replacing the broken @mykin-ai fork
- add AssistantConversationService orchestrating one press-to-talk turn,
  including streaming voice.dialogue.reply text and local SQLite sync of
  voice.command.result via LocalScheduleWriter
- add VoiceTalkButton/TempoAssistantIcon (real audio-level waveform,
  tap-outside-to-dismiss + stop TTS) and wire into HomeScreen alongside
  ScheduleCalendarScreen
- add ExpoLocationProvider for session.hello lat/long

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(realtime): pass client geolocation through session handshake

- thread latitude/longitude from session.hello through SessionHandshake
  into SessionContext (accepted but not consumed yet — no geocoding or
  geofencing reads them)
- restore logging.basicConfig, which the root logger never had, silently
  swallowing existing logger.info calls app-wide
- drop the temporary debug_auth stub now that #217 lands real JWT-backed
  auth (create_auth_router/JwtAccessTokenService), which main.py wires
  in directly

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(realtime): move logging.basicConfig after imports to satisfy E402

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(realtime): label session.hello geolocation as WGS84

- add coordinate_system to SessionHelloPayload (frontend + backend) and
  SessionContext, fixed to the literal "WGS84"
- client sends the raw device coordinates as-is; no client-side
  conversion to GCJ-02 or any other system — whoever consumes
  latitude/longitude downstream decides whether/how to convert

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(assistant): adapt to the merged auth architecture (#218)

- rewrite AppRoot.tsx onto #218's AuthController/useAuth() structure;
  AuthenticatedScheduleRoute now renders HomeScreen (voice-enabled)
  instead of ScheduleCalendarScreen directly, sourcing the access token
  via AuthController.getAccessToken() rather than the token-free
  AuthViewState
- drop the username-display feature (its mount points no longer exist
  post-#218); calendar header shows accountId again
- mock the assistant's native-backed modules in AppRoot's test so the
  suite doesn't try to load a real audio/location native module
- relocate useScheduleCalendar.test.ts / ScheduleCalendarScreen.test.tsx
  from src/ to tests/unit/, matching #218's jest testMatch which no
  longer picks up colocated src/**/*.test.ts files — these were
  silently not running after the merge otherwise

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(assistant): converge onto the shared AuthenticatedWebSocketClient

Voice used to run its own session.hello/session.ready handshake over a
private WebSocketVoiceTransport, duplicating what #218's
AuthenticatedWebSocketClient already does for the app's one shared
connection. Collapse the two:

- add onClose(listener) to AuthenticatedWebSocketClient — it previously
  had no way to notify callers when a ready connection dropped, which
  voice's "连接已断开" UI relies on; only fires for post-ready drops,
  not handshake failures (those already reject connect())
- thread an optional location through connect() → session.hello, since
  voice needs to attach coordinates to the same first frame
- always include the device's IANA timezone in session.hello — it was
  declared in the schema and read server-side (session.py) but never
  actually sent by any client; the backend that consumes it
  (feature/client-timezone-latlong) isn't merged yet, but this unblocks
  it without further frontend changes once it lands
- replace WebSocketVoiceTransport with AuthenticatedVoiceTransport, a
  thin adapter that demuxes the client's generic string|ArrayBuffer
  channel into the JSON-control/binary-audio split VoiceTransportPort
  expects; it does not close the shared connection itself — that stays
  owned by the auth system (AuthInvalidationCoordinator)
- AssistantConversationService.connect() no longer builds/sends
  session.hello itself; the transport is ready by the time it resolves
- drop the now-dead SessionHelloMessage/SessionHelloPayload/
  SessionReadyMessage from contracts/conversation.ts — that handshake
  lives entirely in contracts/authWebSocket.ts now
- AssistantApplicationOptions shrinks to { accountId } — access_token/
  device_id/wsUrl are owned by the shared client, not voice
- add test coverage for onClose, location-in-hello, and timezone-in-hello

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(assistant): request mic permission before opening the stream, make the started-wait rejectable, and dispose connection listeners

Addresses fennoai's review on #229:

- request permission (and check its result) before sending
  voice.stream.start — sending it first left an orphaned active stream
  server-side whenever permission was denied, since there was no
  stream_id yet to send voice.stream.end with; capture.start() failing
  after the stream is already open gets the same voice.stream.end cleanup
- pair streamStartedWaiter with a rejecter so a transport error or
  connection close settles the voice.stream.started wait instead of
  leaving startTurn()/endTurn() pending forever
- connect() now tracks the three connection listeners it registers
  (message/audio/close) and dispose() unsubscribes all of them, not
  just connection.close() — onClose in particular forwards straight to
  the shared AuthenticatedWebSocketClient and was never cleaned up
- AppRoot now calls assistantApplication.dispose() when the memoized
  instance is replaced or the route unmounts
- also clear the location race's losing setTimeout, found while adding
  test coverage for the above (no functional bug, just a dangling timer)
- add AssistantConversationService.test.ts — this class had no test
  coverage before

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(assistant): gate message.ack on local write success, guard transport JSON.parse

message.ack{status:"applied"} was sent unconditionally before the local
SQLite write settled, contradicting AGENTS.md §6's documented ordering.
Unguarded JSON.parse on server frames could also throw through the shared
connection callback on a malformed frame.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants