Skip to content

fix(kapitalbank-uz): fix header formats and missing auth fields#1051

Draft
alex-mextner wants to merge 1 commit into
zenmoney:masterfrom
alex-mextner:fix/kapitalbank-uz-version
Draft

fix(kapitalbank-uz): fix header formats and missing auth fields#1051
alex-mextner wants to merge 1 commit into
zenmoney:masterfrom
alex-mextner:fix/kapitalbank-uz-version

Conversation

@alex-mextner

@alex-mextner alex-mextner commented May 21, 2026

Copy link
Copy Markdown
Contributor

Problem

Users get HTTP 426 (Upgrade Required) from every API call. Error body: {errorCode: 100015, errorDetail: "Неподдерживаемая версия приложения"}. After 426 was fixed, access token expiry triggered a re-auth loop that silently set all tokens to undefined and crashed with response.body.map is not a function.

Root cause

Five issues found by decompiling the official APK (v3.5.1) with jadx:

1. X-App-Version wrong format (hy/p.java — OkHttp client setup)

The app builds this header as StringsKt.substringBefore(versionName, "-"), giving "3.5.1" — not "Android; 3.5.1-build.750release" as the plugin was sending.

2. X-Trace-Info wrong separator and stale requestId (yf0/a.java — header interceptor)

Format is "sessionId=<id> requestId=<uuid>" (space-separated, fresh UUID per request). The plugin used "; " separator and reused a stored requestId.

3. /auth/by-password missing required fields (AuthRequest.java)

Server enforces otpSendingSource and applicationId as required fields, even though the Kotlin side has defaults. Missing fields → VALIDATION_ERROR.

4. refreshToken() used console.assert instead of throwing

console.assert doesn't throw in JS — on a 401 it silently set all ZenMoney state to undefined and returned normally. The retry doScrape() then ran with Authorization: Bearer undefined, received another 401, and crashed with response.body.map is not a function.

5. myIdVerifyResult() spread a string instead of copying errorDetail

{ ...response.body?.errorDetail } when errorDetail is a string produces {0: 'П', 1: 'р', ...}. Changed to { success: false, errorDetail: response.body?.errorDetail }.

Changes

api.js:

  • appVersion strips build suffix via split('-')[0]"3.5.1"
  • generateUuid() added for per-request requestId
  • X-App-Version sends only the semver string (no "Android; " prefix)
  • X-Trace-Info uses space separator with a fresh UUID each call
  • User-Agent bumped to okhttp/5.3.2 (matches APK)
  • /auth/by-password body includes otpSendingSource: 'SMS' and applicationId: 'uz.kapitalbank.kbonline'
  • assertVersionSupported() added for auth endpoints (426 → TemporaryError)
  • refreshToken() throws TemporaryError on non-ok response
  • myIdVerifyResult() error object fixed

index.js:

  • TemporaryError was missing from imports (caused ReferenceError at runtime)
  • Import refreshToken; retry with refreshToken() before full re-auth. Full updateToken() only if refresh fails
  • isResident coerced to boolean (preferences delivers XML strings)

Codex review findings

Codex flagged X-App-Version format and X-Trace-Info separator as potential regressions. Both are false positives — the correct formats were confirmed directly from APK source (hy/p.java, yf0/a.java).

Diagnosed by decompiling the official APK (v3.5.1) with jadx.

--- api.js ---

Header fixes (hy/p.java + yf0/a.java):
- X-App-Version: was "Android; 3.5.1-build.750release"; server expects
  "3.5.1" — uses StringsKt.substringBefore(versionName, "-")
- X-Trace-Info: separator was "; ", should be " "; requestId must be a
  fresh UUID per request, not a stored value
- User-Agent: bumped to okhttp/5.3.2 (matches APK)

Auth body fix (AuthRequest.java):
- /auth/by-password was missing otpSendingSource ("SMS") and
  applicationId ("uz.kapitalbank.kbonline") — required by server even
  though Kotlin side has defaults

assertVersionSupported() added: surfaces HTTP 426 as TemporaryError
instead of crashing on response.body access.

refreshToken() now throws TemporaryError on non-ok response instead of
asserting: console.assert doesn't throw — a 401 was silently setting all
ZenMoney state to undefined and returning normally, causing the retry
doScrape() to run with "Bearer undefined" tokens.

myIdVerifyResult() error handling: was spreading errorDetail string
({ ...string } = char indices, not properties); changed to
{ success: false, errorDetail: response.body?.errorDetail }.

--- index.js ---

- Import refreshToken; retry with refreshToken() before full re-auth
  (password + SMS). Full updateToken() only triggers if refresh fails.
- isResident from XML preferences is a string ("true"/"false"); coerce
  to boolean before sending to myid.uz API.
- Import TemporaryError (was missing; caused ReferenceError at runtime).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alex-mextner alex-mextner force-pushed the fix/kapitalbank-uz-version branch from 9b649f8 to c675033 Compare May 21, 2026 16:23
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.

1 participant