Skip to content

Fix re-authentication in retry logic by resolving auth endpoint via Bag()#448

Open
birmacher wants to merge 7 commits intomajd:mainfrom
birmacher:main
Open

Fix re-authentication in retry logic by resolving auth endpoint via Bag()#448
birmacher wants to merge 7 commits intomajd:mainfrom
birmacher:main

Conversation

@birmacher
Copy link

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

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>
Copilot AI review requested due to automatic review settings February 18, 2026 12:01
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 when authenticateAccount is missing/empty.
  • Update retry re-auth flows (purchase, download, get-version-metadata, list-versions) to call Bag() and pass the resolved auth endpoint into Login().
  • 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>
Comment on lines -45 to +55
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,
})
Copy link
Owner

Choose a reason for hiding this comment

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

Nice, good catch. I had missed this before.

Comment on lines +35 to +38
authEndpoint := res.Data.URLBag.AuthEndpoint
if authEndpoint == "" {
authEndpoint = fmt.Sprintf("https://%s%s", PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathAuthenticate)
}
Copy link
Owner

Choose a reason for hiding this comment

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

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?

Copy link
Author

Choose a reason for hiding this comment

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

I've removed it and instead use a DefaultAuthEndpoint constant directly in the retry paths.

Copy link
Owner

Choose a reason for hiding this comment

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

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?

birmacher and others added 5 commits February 21, 2026 23:41
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>
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.

3 participants