Fix re-authentication in retry logic by resolving auth endpoint via Bag()#448
Fix re-authentication in retry logic by resolving auth endpoint via Bag()#448
Conversation
The retry logic in purchase, download, get-version-metadata, and list-versions called Login() without an Endpoint when re-authenticating after a password token expiry. This caused silent failures because the auth endpoint was empty. - Add Bag() call before Login() in all retry blocks to resolve the auth endpoint dynamically - Fall back to hardcoded auth endpoint when bag.xml omits authenticateAccount - Detect "Your password has changed." customer message as a token expiry trigger alongside failure type 2034 - Increase download retry attempts to 3 to handle license-required then token-expired chain Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes App Store CLI re-authentication during retries by resolving the auth endpoint from bag.xml before calling Login(), preventing silent re-auth failures when the endpoint is empty.
Changes:
- Add a hardcoded fallback auth endpoint and make
Bag()return it whenauthenticateAccountis missing/empty. - Update retry re-auth flows (purchase, download, get-version-metadata, list-versions) to call
Bag()and pass the resolved auth endpoint intoLogin(). - Treat
"Your password has changed."as a password-token-expiry signal and increase download retry attempts to 3.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/appstore/constants.go | Adds a password-changed customer message constant and a fallback authenticate endpoint constant. |
| pkg/appstore/appstore_purchase.go | Expands token-expiry detection to include the new customer message trigger. |
| pkg/appstore/appstore_bag.go | Falls back to the hardcoded authenticate endpoint when bag.xml omits it. |
| pkg/appstore/appstore_bag_test.go | Adds coverage for the empty-authenticateAccount fallback behavior. |
| cmd/purchase.go | Re-auth retry now resolves auth endpoint via Bag() before Login(). |
| cmd/list_versions.go | Re-auth retry now resolves auth endpoint via Bag() before Login(). |
| cmd/get_version_metadata.go | Re-auth retry now resolves auth endpoint via Bag() before Login(). |
| cmd/download.go | Re-auth retry now resolves auth endpoint via Bag() before Login(); increases retry attempts to 3. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…hanged test - Rename PrivateAppStoreAPIPathAuthenticate to store only the path (consistent with other *APIPath* constants), constructing the full URL in the Bag() fallback - Add test for CustomerMessagePasswordChanged triggering ErrPasswordTokenExpired in purchaseWithParams Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{Email: acc.Email, Password: acc.Password}) | ||
| bagOutput, err := dependencies.AppStore.Bag(appstore.BagInput{}) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get bag: %w", err) | ||
| } | ||
|
|
||
| loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{ | ||
| Email: acc.Email, | ||
| Password: acc.Password, | ||
| Endpoint: bagOutput.AuthEndpoint, | ||
| }) |
There was a problem hiding this comment.
Nice, good catch. I had missed this before.
pkg/appstore/appstore_bag.go
Outdated
| authEndpoint := res.Data.URLBag.AuthEndpoint | ||
| if authEndpoint == "" { | ||
| authEndpoint = fmt.Sprintf("https://%s%s", PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathAuthenticate) | ||
| } |
There was a problem hiding this comment.
I'm not so sure about this. The reason we retrieve the auth endpoint from the bag now is that the previous hardcoded endpoint was unstable and started failing as of a few weeks ago. What is the reason to have the fallback here? Does the bag endpoint omit the auth endpoint in some cases?
There was a problem hiding this comment.
I've removed it and instead use a DefaultAuthEndpoint constant directly in the retry paths.
There was a problem hiding this comment.
I meant why do we need to use the default auth endpoint in first place? Can't the retry use the endpoint from the bag, or have you encountered cases where the bag was omitting the endpoints?
Apple now returns HTTP 200 with FailureType "5002" for already-purchased apps instead of HTTP 500. Also treat FailureType "1008" (device verification failed) as a stale token that triggers re-authentication. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Map FailureType "5002" (license already exists) and "1008" (device verification failed) to ErrPasswordTokenExpired in the download flow, triggering re-auth retry consistent with the purchase fix in 438e8e8. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
applyPatches opened the destination file with O_CREATE|O_WRONLY but no O_TRUNC. On re-download, the existing .ipa (which includes sinf data from ReplicateSinf) is larger than what applyPatches writes (zip entries + metadata, no sinf yet). The leftover bytes at the end corrupt the ZIP end-of-central-directory record, causing ReplicateSinf's zip.OpenReader to fail with "failed to open zip reader". Fix: add O_TRUNC to truncate the file before writing, and add a missing defer dstFile.Close() to prevent file descriptor leaks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the user already owns an app, purchase should report success (with alreadyOwned=true) instead of failing with an error. Applied to both the standalone purchase command and the --purchase flag in the download command. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The bag response from init.itunes.apple.com/bag.xml does not contain an authenticateAccount key, so the fallback was firing 100% of the time. Replace the Bag()+Login() pattern in retry paths with Login() using a DefaultAuthEndpoint constant directly, avoiding an unnecessary network round-trip. The cmd/auth.go login flow still uses Bag() as before. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The retry logic in purchase, download, get-version-metadata, and list-versions called Login() without an Endpoint when re-authenticating after a password token expiry. This caused re-auth to silently fail because the auth endpoint was empty.
Add Bag() call before Login() in all retry blocks to resolve the auth endpoint dynamically
Fall back to hardcoded auth endpoint when bag.xml omits authenticateAccount
Detect "Your password has changed." customer message as a token expiry trigger alongside failure type 2034
Increase download retry attempts to 3 to handle license-required → token-expired chain