Fixed
- Apple rate limits and auth-CDN flakes no longer surface as "wrong password". SRP
/signin/completehad a catch-all 4xx branch that mapped every non-409/412 status toFailedLogin("Invalid email/password combination"). Transient 429 rate limits and 403s from rotated routing cookies looked identical to a credential failure. Complete now distinguishes 401 (the real M1 rejection, the one code that actually means wrong password), 429 (ApiErrorwith "Apple is rate limiting authentication"), 400 (ApiErrorwith a kei-bug-or-protocol-change message), and other 4xx (ApiErrorwith the raw status)./signin/init401 is also no longerFailedLogin- init runs before the M1 proof is sent, so a 401 there is a stale auth context (cookies/scnt), not a bad password. (#226) - Bare CloudKit 403 re-authenticates instead of falsely claiming ADP. HTTP 403 at library init unconditionally mapped to
ServiceNotActivatedwith Advanced Data Protection guidance. Real ADP is already caught upstream byi_cdp_enabledin the auth response and by CloudKit body errors (ZONE_NOT_FOUND, ACCESS_DENIED, AUTHENTICATION_FAILED); bare 403 has other causes (rate limits, rotated routing cookies, stale sessions). It now maps toICloudError::SessionExpired { status: 403 }so the sync loop re-auths once, andAUTH_ERROR_THRESHOLD = 3in the download pipeline stops the loop if the 403 really is persistent ADP.SessionExpirednow carries the actual HTTP status, so its Display renders "HTTP 403 from CloudKit" instead of always hardcoding "HTTP 401". (#226) - Transient 5xx/429 on SRP and 2FA endpoints retry instead of failing hard.
/signin/init,/signin/complete,/repair/complete,trigger_push_notification, andsubmit_2fa_codeall retry transient responses up to a sharedAUTH_RETRY_CONFIG(2 retries, 2s base, 30s max) and honorRetry-Afterwhen Apple supplies one. Persistent 5xx after the budget is exhausted surfaces asApiErrorwith the actual status, notFailedLogin. (#226) - SRP now detects
X-Apple-I-Rscdsession rejections. Apple sometimes returns HTTP 200 with this header set to 401 or 403 to signal a hidden session rejection. The check was already wired into/validateand/accountLogin, but SRP init and complete used to treat the 200 as a valid handshake. Both now surfaceAuthError::ServiceErrorwith coderscd_401/rscd_403. (#226) - CloudKit retry loop honors
Retry-After.HttpStatusErrorcaptures the header (delta-seconds only, capped at 120s),classify_api_errorreturns a newRetryAfter(Duration)action on 429/5xx, andretry_with_backoffuses the server-provided delay in place of the exponential schedule for that attempt. (#226)
Changed
AuthError::is_rate_limitedrenamed tois_transient_apple_failureand broadened from HTTP 503 only to 429 + all 5xx. After retries exhaust, the auth layer surfaces "Apple's auth service is returning transient errors (HTTP 429/5xx). Wait a few minutes and retry" context; previously only 503 got the back-off hint. Internal to the binary, no external API impact. (#226)
Full changelog: CHANGELOG.md