Skip to content

Add JWT expiry warning and account-stream heartbeat/staleness detection (fixes #225)#236

Merged
wjllance merged 1 commit into
mainfrom
fix-225-jwt-heartbeat
Jul 13, 2026
Merged

Add JWT expiry warning and account-stream heartbeat/staleness detection (fixes #225)#236
wjllance merged 1 commit into
mainfrom
fix-225-jwt-heartbeat

Conversation

@wjllance

Copy link
Copy Markdown
Owner

Summary

Fixes #225. Two related durability gaps in the live maker:

  1. JWT had no renewal and no early warning. Reconnects and every REST call re-read credentials from disk; once the token expired they were rejected and the bot halted. Combined with the 23h50m WS rotation, token lifetime was effectively the hard cap on run duration. Worse, Credentials::is_expired() / remaining_seconds() were computed purely from stored created_at + validity_seconds metadata — env-var tokens are tracked as ~1 year, so the local view could be wildly optimistic versus a real 24h JWT.
  2. The account stream had no heartbeat/staleness detection. On a half-open TCP connection no error or close frame ever arrives, so AccountStreamHealth::is_healthy() stayed true forever and the runtime never froze/reconnected.

JWT expiry

  • Credentials now decodes the JWT exp claim (best-effort, no signature verification — malformed/opaque tokens fall back to the old metadata). is_expired, remaining_seconds, and expires_at_string clamp to whichever expiry (JWT vs. metadata) comes first, so the venue's real cutoff wins.
  • The live maker monitors remaining token lifetime each loop (throttled to once/60s) and emits a risk notification through the existing MakerNotifier: warning under 2h, escalating to critical under 15m, alerted once per severity band. The message points the operator at standx auth login.

Account-stream heartbeat/staleness

  • Added a client-side ping (30s) and a read-side idle timeout (90s) to AccountStream, mirroring the market websocket.rs heartbeat pattern. Any inbound frame (data/ping/pong) resets the idle deadline; a silent half-open connection trips the timeout, which calls mark_unhealthy and emits AccountEvent::Disconnected. This feeds the existing runtime freeze → cleanup → reconnect path.

Test plan

  • cargo fmt -- --check — clean
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo test -p standx-sdk / -p standx-cli / -p standx-maker — all pass
  • New unit tests:
    • JWT exp decoding (numeric/missing/non-numeric/opaque), and that a short JWT exp overrides optimistic ~1yr metadata for both is_expired and remaining_seconds.
    • token_expiry_level severity banding + ordering used for escalation.
    • idle_connection_is_marked_unhealthy: drives a WS server that authenticates then goes silent; asserts the stream reports an "idle" Disconnected and health flips unhealthy.

Follow-up

  • No JWT renewal endpoint exists in this codebase. Tokens are supplied out-of-band (standx auth login pastes a JWT; there is no challenge/refresh flow in the SDK). Real background renewal (lead-time refresh + retry + failure alert) is therefore left as a follow-up pending venue auth API details. This PR ships the early-warning path so an operator can re-authenticate before the token lapses. Once a refresh endpoint is available, the per-cycle monitor is the natural hook to trigger it.

Fixes #225

🤖 Generated with Claude Code

Fixes #225. Token lifetime was effectively the hard cap on maker run
duration with no early warning, and the account stream could sit on a
half-open TCP connection forever while reporting healthy.

JWT expiry:
- Decode the JWT `exp` claim (best-effort, no signature check) in
  Credentials. `is_expired`, `remaining_seconds`, and
  `expires_at_string` now honour the real venue cutoff and clamp the
  optimistic local metadata (notably env-var tokens tracked as ~1 year).
- The live maker now monitors remaining token lifetime each loop
  (throttled to 60s) and emits a warning risk notification through the
  existing notifier at <2h, escalating to critical at <15m, once per
  severity band. No renewal endpoint exists in the codebase, so this
  ships the warning + documented gap rather than a fabricated refresh.

Account-stream heartbeat/staleness:
- Add a client-side ping (30s) plus a read-side idle timeout (90s) to
  the account stream, mirroring the market websocket heartbeat pattern.
  Any inbound frame resets the idle deadline; on a silent half-open
  connection the timeout marks AccountStreamHealth unhealthy and emits a
  Disconnected event, so the runtime freeze/reconnect path triggers.

Tests: JWT exp parsing/threshold clamping, token-expiry severity bands,
and an idle-connection staleness test driving a silent WS server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wjllance
wjllance force-pushed the fix-225-jwt-heartbeat branch from a447021 to 966ce37 Compare July 13, 2026 08:25
@wjllance
wjllance merged commit 26315db into main Jul 13, 2026
7 checks passed
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.

[P1][Bug] JWT 无续期(token 生命周期=运行时长上限)+ 账户流无心跳/staleness 检测

2 participants