From 7a5e6a055776c15746749c6d6997b51fc48d1c1c Mon Sep 17 00:00:00 2001 From: wl Date: Sun, 7 Jun 2026 06:15:51 +0800 Subject: [PATCH 1/3] fix: update auth endpoint for Apple's new auth.itunes.apple.com domain Apple's authentication service moved the endpoint from buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate to auth.itunes.apple.com/auth/v1/native/fast, and simultaneously moved the `authenticateAccount` key in the Bag XML from inside the `urlBag` dict to the root level. Changes: - bagResult: add root-level `authenticateAccount` plist field so the Bag() function reads it before falling back to urlBag (backward compat) - Bag(): append /fast suffix when the resolved endpoint uses the new auth.itunes.apple.com domain, matching what the Bag XML signals via `auth/v1/native: ["fast"]` - constants: add PrivateAuthDomain / PrivateAuthPathNative for the new endpoint - loginRequest: use the new auth endpoint as the hardcoded fallback (replaces the implicit empty-string which caused "something went wrong" when Bag lookup returned nothing) Without this fix every login attempt returns HTTP 200 with an empty body in ~1 ms, which the code surfaces as "something went wrong". --- pkg/appstore/appstore_bag.go | 14 ++++++++++++-- pkg/appstore/appstore_login.go | 2 +- pkg/appstore/constants.go | 3 +++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pkg/appstore/appstore_bag.go b/pkg/appstore/appstore_bag.go index 29108a2f..dfaefced 100644 --- a/pkg/appstore/appstore_bag.go +++ b/pkg/appstore/appstore_bag.go @@ -32,13 +32,23 @@ func (t *appstore) Bag(input BagInput) (BagOutput, error) { return BagOutput{}, fmt.Errorf("received unexpected status code: %d", res.StatusCode) } + endpoint := res.Data.AuthEndpoint + if endpoint == "" { + endpoint = res.Data.URLBag.AuthEndpoint + } + // The new auth endpoint base requires the /fast sub-path + if strings.Contains(endpoint, "auth.itunes.apple.com") && !strings.HasSuffix(endpoint, "/fast") { + endpoint = endpoint + "/fast" + } + return BagOutput{ - AuthEndpoint: res.Data.URLBag.AuthEndpoint, + AuthEndpoint: endpoint, }, nil } type bagResult struct { - URLBag urlBag `plist:"urlBag,omitempty"` + URLBag urlBag `plist:"urlBag,omitempty"` + AuthEndpoint string `plist:"authenticateAccount,omitempty"` } type urlBag struct { diff --git a/pkg/appstore/appstore_login.go b/pkg/appstore/appstore_login.go index 419fdeaa..9f6c2c1e 100644 --- a/pkg/appstore/appstore_login.go +++ b/pkg/appstore/appstore_login.go @@ -160,7 +160,7 @@ func (t *appstore) parseLoginResponse(res *http.Result[loginResult], attempt int func (t *appstore) loginRequest(email, password, authCode, guid, endpoint string, attempt int) http.Request { return http.Request{ Method: http.MethodPOST, - URL: endpoint, + URL: util.IfEmpty(endpoint, fmt.Sprintf("https://%s%s", PrivateAuthDomain, PrivateAuthPathNative)), ResponseFormat: http.ResponseFormatXML, Headers: map[string]string{ "Content-Type": "application/x-www-form-urlencoded", diff --git a/pkg/appstore/constants.go b/pkg/appstore/constants.go index 66201514..0c1b8b42 100644 --- a/pkg/appstore/constants.go +++ b/pkg/appstore/constants.go @@ -25,6 +25,9 @@ const ( PrivateAppStoreAPIPathPurchase = "/WebObjects/MZFinance.woa/wa/buyProduct" PrivateAppStoreAPIPathDownload = "/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct" + PrivateAuthDomain = "auth." + iTunesAPIDomain + PrivateAuthPathNative = "/auth/v1/native/fast" + HTTPHeaderStoreFront = "X-Set-Apple-Store-Front" HTTPHeaderPod = "pod" From 5eeda954b8f2bde58908fbdf2e45b2143129e3ea Mon Sep 17 00:00:00 2001 From: wl Date: Sun, 7 Jun 2026 06:16:02 +0800 Subject: [PATCH 2/3] fix: remove stale zero-byte cookie lock file on startup The juju/persistent-cookiejar library (via juju/go4/lock) creates a lock file at .lock, writing the owner PID as JSON. If a process is killed between file creation (O_TRUNC) and the PID write, a 0-byte file remains. On the next startup the library skips PID-based stale detection (only runs when fi.Size() > 0) and falls back to os.OpenFile with O_EXCL, which fails because the file already exists. The result is "cannot load cookies: file locked for too long; giving up" on every subsequent invocation until the file is manually removed. Fix: in newCookieJar(), stat the lock path before opening the jar and remove the file if it is zero bytes. Zero-byte lock files are always stale because a live process always writes its PID immediately after creating the file. --- cmd/common.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/common.go b/cmd/common.go index 3b3a3dec..32465026 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -54,8 +54,16 @@ func newLogger(format OutputFormat, verbose bool) log.Logger { // newCookieJar returns a new cookie jar instance. func newCookieJar(machine machine.Machine) http.CookieJar { + cookiePath := filepath.Join(machine.HomeDirectory(), ConfigDirectoryName, CookieJarFileName) + // Remove stale zero-byte lock files. The juju/go4/lock library only does + // PID-based stale detection when the file has content; a 0-byte lock file + // (left by a process killed between O_TRUNC and PID write) blocks forever. + lockPath := cookiePath + ".lock" + if fi, err := os.Stat(lockPath); err == nil && fi.Size() == 0 { + os.Remove(lockPath) + } return util.Must(cookiejar.New(&cookiejar.Options{ - Filename: filepath.Join(machine.HomeDirectory(), ConfigDirectoryName, CookieJarFileName), + Filename: cookiePath, })) } From 70863e0ef5f1ec9e996d557a3ba8f004ce88b85c Mon Sep 17 00:00:00 2001 From: wl Date: Fri, 12 Jun 2026 11:47:31 +0800 Subject: [PATCH 3/3] fix: native auth endpoint requires trailing slash (/auth/v1/native/fast/) Without the trailing slash Apple returns 301 + an HTML redirect page, so the Configurator UA appears "blocked" at login and the plist parser fails. With the slash, the default Configurator/2.17 UA logs in and mints a commerce-grade passwordToken that buyProduct accepts end-to-end (purchase + download) with no anisette / kbsync / X-Apple-ActionSignature / device signature. - normalize the Bag-resolved auth endpoint to always end with /fast/ - PrivateAuthPathNative fallback gains the trailing slash - surface AMD-Action::SP as a clear "account requires browser sign-in" error --- pkg/appstore/appstore_bag.go | 15 ++++++++++++--- pkg/appstore/appstore_login.go | 2 ++ pkg/appstore/constants.go | 3 ++- pkg/http/constants.go | 4 ++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pkg/appstore/appstore_bag.go b/pkg/appstore/appstore_bag.go index dfaefced..d138ec4f 100644 --- a/pkg/appstore/appstore_bag.go +++ b/pkg/appstore/appstore_bag.go @@ -36,9 +36,18 @@ func (t *appstore) Bag(input BagInput) (BagOutput, error) { if endpoint == "" { endpoint = res.Data.URLBag.AuthEndpoint } - // The new auth endpoint base requires the /fast sub-path - if strings.Contains(endpoint, "auth.itunes.apple.com") && !strings.HasSuffix(endpoint, "/fast") { - endpoint = endpoint + "/fast" + // The new auth endpoint requires the /fast sub-path WITH a trailing slash. + // Without the trailing slash Apple returns 301 + an HTML redirect page + // (which the plist parser chokes on), and only the appstored UA gets a + // (download-grade) token. With the slash, the Configurator UA mints a + // commerce-grade token that buyProduct accepts. + if strings.Contains(endpoint, "auth.itunes.apple.com") { + if !strings.HasSuffix(endpoint, "/fast") && !strings.HasSuffix(endpoint, "/fast/") { + endpoint = endpoint + "/fast" + } + if !strings.HasSuffix(endpoint, "/") { + endpoint = endpoint + "/" + } } return BagOutput{ diff --git a/pkg/appstore/appstore_login.go b/pkg/appstore/appstore_login.go index 9f6c2c1e..bf9114f7 100644 --- a/pkg/appstore/appstore_login.go +++ b/pkg/appstore/appstore_login.go @@ -144,6 +144,8 @@ func (t *appstore) parseLoginResponse(res *http.Result[loginResult], attempt int err = ErrAuthCodeRequired } else if res.Data.FailureType == "" && res.Data.CustomerMessage == CustomerMessageAccountDisabled { err = NewErrorWithMetadata(errors.New("account is disabled"), res) + } else if res.Data.FailureType == "" && res.Data.CustomerMessage == CustomerMessageActionSignInPage { + err = NewErrorWithMetadata(errors.New("account requires browser sign-in (2FA or Apple ID review required)"), res) } else if res.Data.FailureType != "" { if res.Data.CustomerMessage != "" { err = NewErrorWithMetadata(errors.New(res.Data.CustomerMessage), res) diff --git a/pkg/appstore/constants.go b/pkg/appstore/constants.go index 0c1b8b42..b7a12f0f 100644 --- a/pkg/appstore/constants.go +++ b/pkg/appstore/constants.go @@ -13,6 +13,7 @@ const ( CustomerMessageAccountDisabled = "Your account is disabled." CustomerMessageSubscriptionRequired = "Subscription Required" CustomerMessagePasswordChanged = "Your password has changed." + CustomerMessageActionSignInPage = "AMD-Action::SP" iTunesAPIDomain = "itunes.apple.com" iTunesAPIPathSearch = "/search" @@ -26,7 +27,7 @@ const ( PrivateAppStoreAPIPathDownload = "/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct" PrivateAuthDomain = "auth." + iTunesAPIDomain - PrivateAuthPathNative = "/auth/v1/native/fast" + PrivateAuthPathNative = "/auth/v1/native/fast/" HTTPHeaderStoreFront = "X-Set-Apple-Store-Front" HTTPHeaderPod = "pod" diff --git a/pkg/http/constants.go b/pkg/http/constants.go index 8f9c9f2f..0bf58af8 100644 --- a/pkg/http/constants.go +++ b/pkg/http/constants.go @@ -8,5 +8,9 @@ const ( ) const ( + // Configurator UA mints a commerce-grade password token (the "Aw..." token) + // that buyProduct accepts. The appstored UA only yields a download-grade + // token that buyProduct rejects with failureType 2034. Requires the auth + // endpoint to carry the trailing slash (see appstore_bag.go). DefaultUserAgent = "Configurator/2.17 (Macintosh; OS X 15.2; 24C5089c) AppleWebKit/0620.1.16.11.6" )