diff --git a/AGENTS.md b/AGENTS.md index 276ab20..f636d57 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,7 +137,7 @@ The backend proxies the bag endpoint via `GET /api/bag?guid=` using No - IndexedDB for credential storage (via `idb`) - `libcurl.js` (WASM) for browser-side TLS 1.3 via Mbed TLS — connects through Wisp protocol - `appleRequest()` in `frontend/src/apple/request.ts` wraps `libcurl.fetch` for all Apple API calls and forces HTTP/1.1 (`_libcurl_http_version: 1.1`) -- Bag endpoint (`frontend/src/apple/bag.ts`) uses backend proxy (`/api/bag`) and falls back to `https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate` when `authenticateAccount` is missing or bag fetch fails +- Bag endpoint (`frontend/src/apple/bag.ts`) uses backend proxy (`/api/bag`) and falls back to the native fast auth endpoint `https://auth.itunes.apple.com/auth/v1/native/fast` when `authenticateAccount` is missing or bag fetch fails. It prefers the top-level `authenticateAccount` over the `urlBag` entry, and appends the `/fast` sub-path to any `auth.itunes.apple.com` endpoint that lacks it (the native auth flow requires `/fast`) - Authentication (`frontend/src/apple/authenticate.ts`) resolves bag endpoint, then sets `guid` via URL query manipulation to avoid duplicate/malformed query parameters - Plist build/parse (`frontend/src/apple/plist.ts`) uses native XML builder and browser-native `DOMParser` - Cookie helper (`frontend/src/apple/cookies.ts`) — `extractAndMergeCookies(rawHeaders, existingCookies)` replaces the repeated extract-and-merge pattern across all Apple protocol files diff --git a/frontend/src/apple/bag.ts b/frontend/src/apple/bag.ts index f693aab..702c013 100644 --- a/frontend/src/apple/bag.ts +++ b/frontend/src/apple/bag.ts @@ -5,8 +5,19 @@ export interface BagOutput { authURL: string; } +// The native "fast" auth endpoint. Apple's newer bag responses point auth at +// auth.itunes.apple.com, and the native auth flow requires the /fast sub-path. export const defaultAuthURL = - "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate"; + "https://auth.itunes.apple.com/auth/v1/native/fast"; + +// Apple's newer auth endpoint base (auth.itunes.apple.com) requires the /fast +// sub-path. Append it when missing so the native auth flow resolves correctly. +function normalizeAuthURL(url: string): string { + if (url.includes("auth.itunes.apple.com") && !url.endsWith("/fast")) { + return `${url}/fast`; + } + return url; +} // Fetches the bag via the backend proxy. // The backend fetches it using Node.js native HTTPS. @@ -27,14 +38,12 @@ export async function fetchBag(deviceId: string): Promise { const xml = await resp.text(); const dict = parsePlist(xml) as Record; - // authenticateAccount may be at top level or inside a urlBag dict - let authURL: string | undefined; - const urlBag = dict.urlBag as Record | undefined; - if (urlBag) { - authURL = urlBag.authenticateAccount as string | undefined; - } + // authenticateAccount may be at top level or inside a urlBag dict. + // Prefer the top-level value, falling back to the urlBag entry. + let authURL = dict.authenticateAccount as string | undefined; if (!authURL) { - authURL = dict.authenticateAccount as string | undefined; + const urlBag = dict.urlBag as Record | undefined; + authURL = urlBag?.authenticateAccount as string | undefined; } if (!authURL) { @@ -44,7 +53,7 @@ export async function fetchBag(deviceId: string): Promise { return { authURL: defaultAuthURL }; } - return { authURL }; + return { authURL: normalizeAuthURL(authURL) }; } catch (error) { console.warn( `[Bag] Failed to fetch/parse bag, using default auth endpoint: ${ diff --git a/frontend/tests/apple/bag.test.ts b/frontend/tests/apple/bag.test.ts index cba2b75..6093c97 100644 --- a/frontend/tests/apple/bag.test.ts +++ b/frontend/tests/apple/bag.test.ts @@ -29,6 +29,67 @@ describe("apple/bag", () => { ); }); + it("prefers the top-level authenticateAccount over urlBag", async () => { + const xml = buildPlist({ + authenticateAccount: + "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate", + urlBag: { + authenticateAccount: "https://example.com/should-not-be-used", + }, + }); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + text: async () => xml, + }), + ); + + const result = await fetchBag("aabbccddeeff"); + + expect(result.authURL).toBe( + "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate", + ); + }); + + it("appends /fast to auth.itunes.apple.com endpoints", async () => { + const xml = buildPlist({ + authenticateAccount: "https://auth.itunes.apple.com/auth/v1/native", + }); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + text: async () => xml, + }), + ); + + const result = await fetchBag("aabbccddeeff"); + + expect(result.authURL).toBe( + "https://auth.itunes.apple.com/auth/v1/native/fast", + ); + }); + + it("does not double-append /fast when already present", async () => { + const xml = buildPlist({ + authenticateAccount: "https://auth.itunes.apple.com/auth/v1/native/fast", + }); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + text: async () => xml, + }), + ); + + const result = await fetchBag("aabbccddeeff"); + + expect(result.authURL).toBe( + "https://auth.itunes.apple.com/auth/v1/native/fast", + ); + }); + it("falls back when authenticateAccount is missing", async () => { const xml = buildPlist({ urlBag: {