diff --git a/src/lib/garmin.test.ts b/src/lib/garmin.test.ts index ffadf38..943edae 100644 --- a/src/lib/garmin.test.ts +++ b/src/lib/garmin.test.ts @@ -173,17 +173,14 @@ describe("fetchGarminData", () => { json: () => Promise.resolve(mockResponse), }); - await fetchGarminData("/wellness/daily/123", { - oauth2Token: "test-token", - }); + await fetchGarminData("/wellness/daily/123", "test-token"); expect(global.fetch).toHaveBeenCalledWith( - "https://connect.garmin.com/modern/proxy/wellness/daily/123", + "https://connectapi.garmin.com/wellness/daily/123", { headers: { Authorization: "Bearer test-token", - NK: "NT", - "User-Agent": "GCM-iOS-5.7.2.1", + "User-Agent": "GCM-iOS-5.19.1.2", }, }, ); @@ -210,7 +207,7 @@ describe("fetchGarminData", () => { }); await expect( - fetchGarminData("/wellness/daily/123", { oauth2Token: "test-token" }), + fetchGarminData("/wellness/daily/123", "test-token"), ).rejects.toThrow("Garmin API error: 401"); }); @@ -221,7 +218,7 @@ describe("fetchGarminData", () => { }); await expect( - fetchGarminData("/wellness/daily/123", { oauth2Token: "test-token" }), + fetchGarminData("/wellness/daily/123", "test-token"), ).rejects.toThrow("Garmin API error: 403"); }); @@ -232,7 +229,7 @@ describe("fetchGarminData", () => { }); await expect( - fetchGarminData("/wellness/daily/123", { oauth2Token: "test-token" }), + fetchGarminData("/wellness/daily/123", "test-token"), ).rejects.toThrow("Garmin API error: 500"); }); @@ -240,7 +237,7 @@ describe("fetchGarminData", () => { global.fetch = vi.fn().mockRejectedValue(new Error("Network error")); await expect( - fetchGarminData("/wellness/daily/123", { oauth2Token: "test-token" }), + fetchGarminData("/wellness/daily/123", "test-token"), ).rejects.toThrow("Network error"); }); }); @@ -267,7 +264,7 @@ describe("fetchHrvStatus", () => { expect(result).toBe("Balanced"); expect(global.fetch).toHaveBeenCalledWith( - "https://connect.garmin.com/modern/proxy/hrv-service/hrv/2024-01-15", + "https://connectapi.garmin.com/hrv-service/hrv/2024-01-15", expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer test-token", @@ -354,7 +351,7 @@ describe("fetchBodyBattery", () => { yesterdayLow: 25, }); expect(global.fetch).toHaveBeenCalledWith( - "https://connect.garmin.com/modern/proxy/usersummary-service/stats/bodyBattery/dates/2024-01-15", + "https://connectapi.garmin.com/usersummary-service/stats/bodyBattery/dates/2024-01-15", expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer test-token", @@ -457,7 +454,7 @@ describe("fetchIntensityMinutes", () => { expect(result).toBe(75); expect(global.fetch).toHaveBeenCalledWith( - "https://connect.garmin.com/modern/proxy/fitnessstats-service/activity", + "https://connectapi.garmin.com/fitnessstats-service/activity", expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer test-token", diff --git a/src/lib/garmin.ts b/src/lib/garmin.ts index 94a41d5..497268a 100644 --- a/src/lib/garmin.ts +++ b/src/lib/garmin.ts @@ -4,10 +4,15 @@ import { logger } from "@/lib/logger"; import type { GarminTokens, HrvStatus } from "@/types"; -const GARMIN_BASE_URL = "https://connect.garmin.com/modern/proxy"; +// Use connectapi subdomain directly (same as garth library) +const GARMIN_API_URL = "https://connectapi.garmin.com"; -interface GarminApiOptions { - oauth2Token: string; +// Headers matching garth library's http.py USER_AGENT +function getGarminHeaders(oauth2Token: string): Record { + return { + Authorization: `Bearer ${oauth2Token}`, + "User-Agent": "GCM-iOS-5.19.1.2", + }; } export interface BodyBatteryData { @@ -17,14 +22,10 @@ export interface BodyBatteryData { export async function fetchGarminData( endpoint: string, - options: GarminApiOptions, + oauth2Token: string, ): Promise { - const response = await fetch(`${GARMIN_BASE_URL}${endpoint}`, { - headers: { - Authorization: `Bearer ${options.oauth2Token}`, - NK: "NT", - "User-Agent": "GCM-iOS-5.7.2.1", - }, + const response = await fetch(`${GARMIN_API_URL}${endpoint}`, { + headers: getGarminHeaders(oauth2Token), }); if (!response.ok) { @@ -58,12 +59,8 @@ export async function fetchHrvStatus( oauth2Token: string, ): Promise { try { - const response = await fetch(`${GARMIN_BASE_URL}/hrv-service/hrv/${date}`, { - headers: { - Authorization: `Bearer ${oauth2Token}`, - NK: "NT", - "User-Agent": "GCM-iOS-5.7.2.1", - }, + const response = await fetch(`${GARMIN_API_URL}/hrv-service/hrv/${date}`, { + headers: getGarminHeaders(oauth2Token), }); if (!response.ok) { @@ -113,13 +110,9 @@ export async function fetchBodyBattery( ): Promise { try { const response = await fetch( - `${GARMIN_BASE_URL}/usersummary-service/stats/bodyBattery/dates/${date}`, + `${GARMIN_API_URL}/usersummary-service/stats/bodyBattery/dates/${date}`, { - headers: { - Authorization: `Bearer ${oauth2Token}`, - NK: "NT", - "User-Agent": "GCM-iOS-5.7.2.1", - }, + headers: getGarminHeaders(oauth2Token), }, ); @@ -172,13 +165,9 @@ export async function fetchIntensityMinutes( ): Promise { try { const response = await fetch( - `${GARMIN_BASE_URL}/fitnessstats-service/activity`, + `${GARMIN_API_URL}/fitnessstats-service/activity`, { - headers: { - Authorization: `Bearer ${oauth2Token}`, - NK: "NT", - "User-Agent": "GCM-iOS-5.7.2.1", - }, + headers: getGarminHeaders(oauth2Token), }, );