@kaiord/garmin-connect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @kaiord/garmin-connect
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@kaiord/garmin-connect.svg)](https://www.npmjs.com/package/@kaiord/garmin-connect)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Garmin Connect API client for the Kaiord health & fitness data framework. Provides authentication, workout listing, and workout pushing via the Garmin Connect API.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pnpm add @kaiord/garmin-connect
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ### Quick Start
17
+
18
+ ```typescript
19
+ import { createGarminConnectClient } from "@kaiord/garmin-connect";
20
+ import type { KRD } from "@kaiord/core";
21
+
22
+ const { auth, service } = createGarminConnectClient();
23
+
24
+ // Login
25
+ await auth.login("email@example.com", "password");
26
+
27
+ // List workouts
28
+ const workouts = await service.list({ limit: 10 });
29
+
30
+ // Push a KRD workout to Garmin Connect
31
+ const result = await service.push(krd);
32
+ console.log(`Pushed workout: ${result.name} (id: ${result.id})`);
33
+ ```
34
+
35
+ ### Token Persistence
36
+
37
+ ```typescript
38
+ import {
39
+ createGarminConnectClient,
40
+ createFileTokenStore,
41
+ } from "@kaiord/garmin-connect";
42
+
43
+ const tokenStore = createFileTokenStore("./tokens.json");
44
+ const { auth, service } = createGarminConnectClient({ tokenStore });
45
+
46
+ // Login (tokens are saved automatically)
47
+ await auth.login("email@example.com", "password");
48
+
49
+ // On next run, tokens are restored automatically
50
+ // No need to login again if tokens are still valid
51
+ ```
52
+
53
+ ### Custom Cookie-Aware Fetch
54
+
55
+ ```typescript
56
+ import {
57
+ createGarminConnectClient,
58
+ createCookieFetch,
59
+ } from "@kaiord/garmin-connect";
60
+
61
+ const cookieFetch = createCookieFetch();
62
+ const { auth, service } = createGarminConnectClient({ fetchFn: cookieFetch });
63
+ ```
64
+
65
+ ## API
66
+
67
+ ### `createGarminConnectClient(options?): { auth, service }`
68
+
69
+ Creates a Garmin Connect client with authentication and workout service.
70
+
71
+ **Options:**
72
+
73
+ - `fetchFn` - Custom fetch function (defaults to cookie-aware fetch)
74
+ - `tokenStore` - Token persistence store (defaults to in-memory)
75
+
76
+ ### `auth.login(email, password): Promise<void>`
77
+
78
+ Authenticates with Garmin Connect via SSO.
79
+
80
+ ### `auth.is_authenticated(): boolean`
81
+
82
+ Checks if the client has valid authentication tokens.
83
+
84
+ ### `auth.export_tokens(): Promise<TokenData>`
85
+
86
+ Exports current tokens for external storage.
87
+
88
+ ### `auth.restore_tokens(tokens): Promise<void>`
89
+
90
+ Restores previously exported tokens.
91
+
92
+ ### `service.list(options?): Promise<WorkoutSummary[]>`
93
+
94
+ Lists workouts from Garmin Connect.
95
+
96
+ ### `service.push(krd): Promise<PushResult>`
97
+
98
+ Pushes a KRD structured workout to Garmin Connect.
99
+
100
+ ### `createCookieFetch(): typeof fetch`
101
+
102
+ Creates a cookie-aware fetch wrapper for SSO authentication flows.
103
+
104
+ ### `createFileTokenStore(path): TokenStore`
105
+
106
+ Creates a file-based token store for persistent authentication.
107
+
108
+ ### `createMemoryTokenStore(): TokenStore`
109
+
110
+ Creates an in-memory token store (tokens lost on process exit).
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,51 @@
1
+ import * as _kaiord_core from '@kaiord/core';
2
+ import { WorkoutService, Logger, TokenStore, AuthProvider } from '@kaiord/core';
3
+ export { ListOptions, PushResult, TokenData, TokenStore, WorkoutSummary } from '@kaiord/core';
4
+
5
+ type OAuth1Token = {
6
+ oauth_token: string;
7
+ oauth_token_secret: string;
8
+ };
9
+ type OAuth2Token = {
10
+ access_token: string;
11
+ refresh_token: string;
12
+ token_type: string;
13
+ expires_in: number;
14
+ refresh_token_expires_in: number;
15
+ expires_at: number;
16
+ };
17
+ type GarminHttpClient = {
18
+ get: <T>(url: string) => Promise<T>;
19
+ post: <T>(url: string, body: unknown) => Promise<T>;
20
+ del: <T>(url: string) => Promise<T>;
21
+ setTokens: (oauth1: OAuth1Token, oauth2: OAuth2Token) => void;
22
+ clearTokens: () => void;
23
+ getOAuth2Token: () => OAuth2Token | undefined;
24
+ };
25
+
26
+ type GarminWorkoutClient = Pick<WorkoutService, "push" | "list">;
27
+
28
+ type GarminAuthProviderOptions = {
29
+ logger?: Logger;
30
+ tokenStore?: TokenStore;
31
+ fetchFn?: typeof globalThis.fetch;
32
+ };
33
+ type GarminAuthProviderResult = {
34
+ auth: AuthProvider;
35
+ httpClient: GarminHttpClient;
36
+ };
37
+ declare const createGarminAuthProvider: (options?: GarminAuthProviderOptions) => GarminAuthProviderResult;
38
+
39
+ type GarminConnectClientOptions = GarminAuthProviderOptions;
40
+ declare const createGarminConnectClient: (options?: GarminConnectClientOptions) => {
41
+ auth: _kaiord_core.AuthProvider;
42
+ service: GarminWorkoutClient;
43
+ };
44
+
45
+ declare const createCookieFetch: () => typeof globalThis.fetch;
46
+
47
+ declare const createFileTokenStore: (filePath?: string) => TokenStore;
48
+
49
+ declare const createMemoryTokenStore: () => TokenStore;
50
+
51
+ export { type GarminWorkoutClient, createCookieFetch, createFileTokenStore, createGarminAuthProvider, createGarminConnectClient, createMemoryTokenStore };
package/dist/index.js ADDED
@@ -0,0 +1,530 @@
1
+ import { createConsoleLogger, createServiceAuthError, createServiceApiError, toText } from '@kaiord/core';
2
+ import { createHmac } from 'crypto';
3
+ import OAuth from 'oauth-1.0a';
4
+ import fetchCookie from 'fetch-cookie';
5
+ import { z } from 'zod';
6
+ import { createGarminWriter } from '@kaiord/garmin';
7
+ import { unlink, readFile, mkdir, writeFile } from 'fs/promises';
8
+ import { join, dirname } from 'path';
9
+ import { homedir } from 'os';
10
+
11
+ // src/adapters/auth/garmin-auth-provider.ts
12
+
13
+ // src/adapters/http/urls.ts
14
+ var GARMIN_SSO_ORIGIN = "https://sso.garmin.com";
15
+ var GARMIN_SSO_EMBED = "https://sso.garmin.com/sso/embed";
16
+ var SIGNIN_URL = "https://sso.garmin.com/sso/signin";
17
+ var GC_MODERN = "https://connect.garmin.com/modern";
18
+ var API_BASE = "https://connectapi.garmin.com";
19
+ var OAUTH_URL = `${API_BASE}/oauth-service/oauth`;
20
+ var WORKOUT_URL = `${API_BASE}/workout-service`;
21
+ var OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.json";
22
+ var USER_AGENT_MOBILE = "com.garmin.android.apps.connectmobile";
23
+ var USER_AGENT_BROWSER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";
24
+
25
+ // src/adapters/http/oauth-consumer.ts
26
+ var fetchOAuthConsumer = async (fetchFn) => {
27
+ const res = await fetchFn(OAUTH_CONSUMER_URL);
28
+ if (!res.ok) {
29
+ throw createServiceAuthError(
30
+ `Failed to fetch OAuth consumer: ${res.status} ${res.statusText}`
31
+ );
32
+ }
33
+ const data = await res.json();
34
+ return { key: data.consumer_key, secret: data.consumer_secret };
35
+ };
36
+ var ACCOUNT_LOCKED_RE = /var status\s*=\s*"([^"]*)"/;
37
+ var PAGE_TITLE_RE = /<title>([^<]*)<\/title>/;
38
+ var checkAccountLocked = (html) => {
39
+ const match = ACCOUNT_LOCKED_RE.exec(html);
40
+ if (match && match[1] === "ACCOUNT_LOCKED") {
41
+ throw createServiceAuthError(
42
+ `Account locked: ${match[1]}. Unlock via Garmin Connect web.`
43
+ );
44
+ }
45
+ };
46
+ var checkPageTitle = (html, logger) => {
47
+ const match = PAGE_TITLE_RE.exec(html);
48
+ if (match?.[1]?.includes("Update Phone Number")) {
49
+ throw createServiceAuthError("Login failed: phone number update required.");
50
+ }
51
+ if (match) {
52
+ logger.debug("Login page title", { title: match[1] });
53
+ }
54
+ };
55
+
56
+ // src/adapters/http/sso-login.ts
57
+ var CSRF_RE = /name="_csrf"\s+value="(.+?)"/;
58
+ var TICKET_RE = /ticket=([^"]+)"/;
59
+ var fetchCsrfToken = async (fetchFn) => {
60
+ const signinParams = new URLSearchParams({
61
+ id: "gauth-widget",
62
+ embedWidget: "true",
63
+ locale: "en",
64
+ gauthHost: GARMIN_SSO_EMBED
65
+ });
66
+ const csrfRes = await fetchFn(`${SIGNIN_URL}?${signinParams}`);
67
+ if (!csrfRes.ok) {
68
+ throw createServiceAuthError(
69
+ `SSO login page returned ${csrfRes.status}: ${csrfRes.statusText}`
70
+ );
71
+ }
72
+ const csrfHtml = await csrfRes.text();
73
+ const csrfMatch = CSRF_RE.exec(csrfHtml);
74
+ if (!csrfMatch) {
75
+ throw createServiceAuthError("CSRF token not found on login page");
76
+ }
77
+ return csrfMatch[1];
78
+ };
79
+ var submitLogin = async (username, password, csrf, fetchFn) => {
80
+ const loginParams = new URLSearchParams({
81
+ id: "gauth-widget",
82
+ embedWidget: "true",
83
+ clientId: "GarminConnect",
84
+ locale: "en",
85
+ gauthHost: GARMIN_SSO_EMBED,
86
+ service: GARMIN_SSO_EMBED,
87
+ source: GARMIN_SSO_EMBED,
88
+ redirectAfterAccountLoginUrl: GARMIN_SSO_EMBED,
89
+ redirectAfterAccountCreationUrl: GARMIN_SSO_EMBED
90
+ });
91
+ const body = new URLSearchParams({
92
+ username,
93
+ password,
94
+ embed: "true",
95
+ _csrf: csrf
96
+ });
97
+ const loginRes = await fetchFn(`${SIGNIN_URL}?${loginParams}`, {
98
+ method: "POST",
99
+ headers: {
100
+ "Content-Type": "application/x-www-form-urlencoded",
101
+ Dnt: "1",
102
+ Origin: GARMIN_SSO_ORIGIN,
103
+ Referer: SIGNIN_URL,
104
+ "User-Agent": USER_AGENT_BROWSER
105
+ },
106
+ body: body.toString()
107
+ });
108
+ return loginRes.text();
109
+ };
110
+ var getLoginTicket = async (username, password, fetchFn, logger) => {
111
+ const embedParams = new URLSearchParams({
112
+ clientId: "GarminConnect",
113
+ locale: "en",
114
+ service: GC_MODERN
115
+ });
116
+ await fetchFn(`${GARMIN_SSO_EMBED}?${embedParams}`);
117
+ const csrf = await fetchCsrfToken(fetchFn);
118
+ const loginHtml = await submitLogin(username, password, csrf, fetchFn);
119
+ checkAccountLocked(loginHtml);
120
+ checkPageTitle(loginHtml, logger);
121
+ const ticketMatch = TICKET_RE.exec(loginHtml);
122
+ if (!ticketMatch) {
123
+ throw createServiceAuthError(
124
+ "Login failed: ticket not found. Check username and password."
125
+ );
126
+ }
127
+ return ticketMatch[1];
128
+ };
129
+ var createOAuthSigner = (consumer) => {
130
+ const oauth = new OAuth({
131
+ consumer,
132
+ signature_method: "HMAC-SHA1",
133
+ hash_function(baseString, key) {
134
+ return createHmac("sha1", key).update(baseString).digest("base64");
135
+ }
136
+ });
137
+ return {
138
+ toHeader: (request, token) => {
139
+ const authorized = token ? oauth.authorize(request, token) : oauth.authorize(request);
140
+ const header = oauth.toHeader(authorized);
141
+ return Object.fromEntries(Object.entries(header));
142
+ }
143
+ };
144
+ };
145
+
146
+ // src/adapters/http/sso-oauth.ts
147
+ var getOAuth1Token = async (ticket, consumer, fetchFn) => {
148
+ const signer = createOAuthSigner(consumer);
149
+ const params = new URLSearchParams({
150
+ ticket,
151
+ "login-url": GARMIN_SSO_EMBED,
152
+ "accepts-mfa-tokens": "true"
153
+ });
154
+ const url = `${OAUTH_URL}/preauthorized?${params}`;
155
+ const headers = signer.toHeader({ url, method: "GET" });
156
+ const res = await fetchFn(url, {
157
+ headers: { ...headers, "User-Agent": USER_AGENT_MOBILE }
158
+ });
159
+ if (!res.ok) {
160
+ throw createServiceAuthError(
161
+ `OAuth1 token request failed: ${res.status} ${res.statusText}`
162
+ );
163
+ }
164
+ const text = await res.text();
165
+ const parsed = new URLSearchParams(text);
166
+ const oauthToken = parsed.get("oauth_token");
167
+ const oauthTokenSecret = parsed.get("oauth_token_secret");
168
+ if (!oauthToken || !oauthTokenSecret) {
169
+ throw createServiceAuthError("OAuth1 token exchange failed");
170
+ }
171
+ return { oauth_token: oauthToken, oauth_token_secret: oauthTokenSecret };
172
+ };
173
+ var exchangeOAuth2 = async (oauth1, consumer, fetchFn) => {
174
+ const signer = createOAuthSigner(consumer);
175
+ const baseUrl = `${OAUTH_URL}/exchange/user/2.0`;
176
+ const token = {
177
+ key: oauth1.oauth_token,
178
+ secret: oauth1.oauth_token_secret
179
+ };
180
+ const authHeader = signer.toHeader({ url: baseUrl, method: "POST" }, token);
181
+ const res = await fetchFn(baseUrl, {
182
+ method: "POST",
183
+ headers: {
184
+ ...authHeader,
185
+ "User-Agent": USER_AGENT_MOBILE,
186
+ "Content-Type": "application/x-www-form-urlencoded"
187
+ }
188
+ });
189
+ if (!res.ok) {
190
+ throw createServiceAuthError(
191
+ `OAuth2 exchange failed: ${res.status} ${res.statusText}`
192
+ );
193
+ }
194
+ const data = await res.json();
195
+ return {
196
+ ...data,
197
+ expires_at: Math.floor(Date.now() / 1e3) + data.expires_in
198
+ };
199
+ };
200
+
201
+ // src/adapters/http/garmin-sso.ts
202
+ var garminSso = async (username, password, logger, fetchFn) => {
203
+ logger.info("Starting Garmin Connect SSO login");
204
+ const consumer = await fetchOAuthConsumer(fetchFn);
205
+ const ticket = await getLoginTicket(username, password, fetchFn, logger);
206
+ logger.debug("SSO ticket obtained");
207
+ const oauth1 = await getOAuth1Token(ticket, consumer, fetchFn);
208
+ logger.debug("OAuth1 token obtained");
209
+ const oauth2 = await exchangeOAuth2(oauth1, consumer, fetchFn);
210
+ logger.info("Garmin Connect SSO login successful");
211
+ return { oauth1, oauth2 };
212
+ };
213
+
214
+ // src/adapters/http/token-refresh.ts
215
+ var createTokenRefreshManager = (fetchFn, logger) => {
216
+ const state = {
217
+ oauth1Token: void 0,
218
+ oauth2Token: void 0,
219
+ consumer: void 0
220
+ };
221
+ let isRefreshing = false;
222
+ let subscribers = [];
223
+ const getConsumer = async () => {
224
+ if (state.consumer) return state.consumer;
225
+ state.consumer = await fetchOAuthConsumer(fetchFn);
226
+ return state.consumer;
227
+ };
228
+ const refreshToken = async () => {
229
+ if (!state.oauth1Token || !state.oauth2Token) {
230
+ throw createServiceApiError("No tokens available for refresh", 401);
231
+ }
232
+ const cons = await getConsumer();
233
+ state.oauth2Token = await exchangeOAuth2(state.oauth1Token, cons, fetchFn);
234
+ logger.info("OAuth2 token refreshed");
235
+ };
236
+ const waitForRefresh = () => new Promise((resolve, reject) => {
237
+ subscribers.push({ resolve, reject });
238
+ });
239
+ const notifySubscribers = () => {
240
+ if (!state.oauth2Token) return;
241
+ const token = state.oauth2Token.access_token;
242
+ subscribers.forEach((s) => {
243
+ s.resolve(token);
244
+ });
245
+ subscribers = [];
246
+ };
247
+ const rejectSubscribers = (error) => {
248
+ subscribers.forEach((s) => {
249
+ s.reject(error);
250
+ });
251
+ subscribers = [];
252
+ };
253
+ return {
254
+ state,
255
+ ensureFreshToken: async () => {
256
+ if (isRefreshing) {
257
+ await waitForRefresh();
258
+ return;
259
+ }
260
+ isRefreshing = true;
261
+ try {
262
+ await refreshToken();
263
+ notifySubscribers();
264
+ } catch (error) {
265
+ rejectSubscribers(error);
266
+ throw error;
267
+ } finally {
268
+ isRefreshing = false;
269
+ }
270
+ }
271
+ };
272
+ };
273
+
274
+ // src/adapters/http/garmin-http-client.ts
275
+ var createGarminHttpClient = (logger, fetchFn = globalThis.fetch) => {
276
+ const refresh = createTokenRefreshManager(fetchFn, logger);
277
+ const makeRequest = (url, init) => {
278
+ if (!refresh.state.oauth2Token) {
279
+ throw createServiceApiError("Token unavailable after refresh", 401);
280
+ }
281
+ return fetchFn(url, {
282
+ ...init,
283
+ headers: {
284
+ ...init?.headers,
285
+ Authorization: `Bearer ${refresh.state.oauth2Token.access_token}`
286
+ }
287
+ });
288
+ };
289
+ const authFetch = async (url, init) => {
290
+ if (!refresh.state.oauth2Token) {
291
+ throw createServiceApiError("Not authenticated", 401);
292
+ }
293
+ if (refresh.state.oauth2Token.expires_at < Math.floor(Date.now() / 1e3)) {
294
+ await refresh.ensureFreshToken();
295
+ }
296
+ const res = await makeRequest(url, init);
297
+ if (res.status === 401) {
298
+ await refresh.ensureFreshToken();
299
+ const retry = await makeRequest(url, init);
300
+ if (!retry.ok) {
301
+ throw createServiceApiError(
302
+ `API request failed after token refresh: ${retry.statusText}`,
303
+ retry.status
304
+ );
305
+ }
306
+ return retry;
307
+ }
308
+ if (!res.ok) {
309
+ throw createServiceApiError(
310
+ `API request failed: ${res.statusText}`,
311
+ res.status
312
+ );
313
+ }
314
+ return res;
315
+ };
316
+ return {
317
+ get: async (url) => {
318
+ const res = await authFetch(url);
319
+ return await res.json();
320
+ },
321
+ post: async (url, body) => {
322
+ const res = await authFetch(url, {
323
+ method: "POST",
324
+ headers: { "Content-Type": "application/json" },
325
+ body: body !== null ? JSON.stringify(body) : void 0
326
+ });
327
+ return await res.json();
328
+ },
329
+ del: async (url) => {
330
+ const res = await authFetch(url, {
331
+ method: "POST",
332
+ headers: { "X-Http-Method-Override": "DELETE" }
333
+ });
334
+ const text = await res.text();
335
+ return text ? JSON.parse(text) : void 0;
336
+ },
337
+ setTokens: (o1, o2) => {
338
+ refresh.state.oauth1Token = o1;
339
+ refresh.state.oauth2Token = o2;
340
+ },
341
+ clearTokens: () => {
342
+ refresh.state.oauth1Token = void 0;
343
+ refresh.state.oauth2Token = void 0;
344
+ refresh.state.consumer = void 0;
345
+ },
346
+ getOAuth2Token: () => refresh.state.oauth2Token
347
+ };
348
+ };
349
+ var createCookieFetch = () => fetchCookie(globalThis.fetch);
350
+ var oauth1TokenSchema = z.object({
351
+ oauth_token: z.string(),
352
+ oauth_token_secret: z.string()
353
+ });
354
+ var oauth2TokenSchema = z.object({
355
+ access_token: z.string(),
356
+ refresh_token: z.string(),
357
+ token_type: z.string(),
358
+ expires_in: z.number(),
359
+ refresh_token_expires_in: z.number(),
360
+ expires_at: z.number()
361
+ });
362
+ var garminTokensSchema = z.object({
363
+ oauth1: oauth1TokenSchema,
364
+ oauth2: oauth2TokenSchema
365
+ });
366
+
367
+ // src/adapters/auth/garmin-auth-provider.ts
368
+ var createGarminAuthProvider = (options) => {
369
+ const logger = options?.logger ?? createConsoleLogger();
370
+ const tokenStore = options?.tokenStore;
371
+ const fetchFn = options?.fetchFn ?? createCookieFetch();
372
+ const httpClient = createGarminHttpClient(logger, fetchFn);
373
+ let currentOAuth1;
374
+ const auth = {
375
+ login: async (username, password) => {
376
+ const result = await garminSso(username, password, logger, fetchFn);
377
+ currentOAuth1 = result.oauth1;
378
+ httpClient.setTokens(result.oauth1, result.oauth2);
379
+ if (tokenStore) {
380
+ await tokenStore.save({
381
+ oauth1: result.oauth1,
382
+ oauth2: result.oauth2
383
+ });
384
+ }
385
+ },
386
+ is_authenticated: () => {
387
+ const token = httpClient.getOAuth2Token();
388
+ if (!token) return false;
389
+ return token.expires_at > Math.floor(Date.now() / 1e3);
390
+ },
391
+ export_tokens: async () => {
392
+ const token = httpClient.getOAuth2Token();
393
+ if (!token || !currentOAuth1) {
394
+ throw createServiceAuthError("No tokens to export");
395
+ }
396
+ return { oauth1: currentOAuth1, oauth2: token };
397
+ },
398
+ restore_tokens: async (data) => {
399
+ const parsed = garminTokensSchema.parse(data);
400
+ currentOAuth1 = parsed.oauth1;
401
+ httpClient.setTokens(parsed.oauth1, parsed.oauth2);
402
+ logger.info("Tokens restored from stored session");
403
+ },
404
+ logout: async () => {
405
+ currentOAuth1 = void 0;
406
+ httpClient.clearTokens();
407
+ if (tokenStore) {
408
+ await tokenStore.clear();
409
+ }
410
+ logger.info("Logged out from Garmin Connect");
411
+ }
412
+ };
413
+ return { auth, httpClient };
414
+ };
415
+
416
+ // src/adapters/mappers/workout-summary.mapper.ts
417
+ var mapToWorkoutSummary = (garminWorkout) => ({
418
+ id: String(garminWorkout.workoutId ?? ""),
419
+ name: garminWorkout.workoutName ?? "Unnamed",
420
+ sport: garminWorkout.sportType?.sportTypeKey ?? "unknown",
421
+ created_at: garminWorkout.createdDate ? new Date(garminWorkout.createdDate).toISOString() : "",
422
+ updated_at: garminWorkout.updatedDate ? new Date(garminWorkout.updatedDate).toISOString() : ""
423
+ });
424
+ var garminWorkoutSummarySchema = z.object({
425
+ workoutId: z.number().or(z.string()),
426
+ workoutName: z.string().optional(),
427
+ sportType: z.object({ sportTypeKey: z.string().optional() }).optional(),
428
+ createdDate: z.number().or(z.string()).optional(),
429
+ updatedDate: z.number().or(z.string()).optional()
430
+ });
431
+ var garminPushResponseSchema = z.object({
432
+ workoutId: z.number().or(z.string()),
433
+ workoutName: z.string().optional()
434
+ });
435
+
436
+ // src/adapters/client/garmin-workout-service.ts
437
+ var pushWorkout = async (krd, httpClient, garminWriter, log) => {
438
+ try {
439
+ log.info("Pushing workout to Garmin Connect");
440
+ const gcnJson = await toText(krd, garminWriter, log);
441
+ const payload = JSON.parse(gcnJson);
442
+ const raw = await httpClient.post(
443
+ `${WORKOUT_URL}/workout`,
444
+ payload
445
+ );
446
+ const result = garminPushResponseSchema.parse(raw);
447
+ return {
448
+ id: String(result.workoutId),
449
+ name: result.workoutName ?? "Workout",
450
+ url: `https://connect.garmin.com/modern/workout/${result.workoutId}`
451
+ };
452
+ } catch (error) {
453
+ throw createServiceApiError("Failed to push workout", void 0, error);
454
+ }
455
+ };
456
+ var listWorkouts = async (httpClient, log, options) => {
457
+ try {
458
+ log.info("Listing workouts from Garmin Connect");
459
+ const start = options?.offset ?? 0;
460
+ const limit = options?.limit ?? 20;
461
+ const params = new URLSearchParams({
462
+ start: String(start),
463
+ limit: String(limit)
464
+ });
465
+ const raw = await httpClient.get(
466
+ `${WORKOUT_URL}/workouts?${params}`
467
+ );
468
+ const workouts = garminWorkoutSummarySchema.array().parse(raw);
469
+ return workouts.map(mapToWorkoutSummary);
470
+ } catch (error) {
471
+ throw createServiceApiError("Failed to list workouts", void 0, error);
472
+ }
473
+ };
474
+ var createGarminWorkoutService = (httpClient, logger) => {
475
+ const log = logger ?? createConsoleLogger();
476
+ const garminWriter = createGarminWriter(log);
477
+ return {
478
+ push: (krd) => pushWorkout(krd, httpClient, garminWriter, log),
479
+ list: (opts) => listWorkouts(httpClient, log, opts)
480
+ };
481
+ };
482
+
483
+ // src/adapters/client/garmin-connect-client.ts
484
+ var createGarminConnectClient = (options) => {
485
+ const { auth, httpClient } = createGarminAuthProvider(options);
486
+ const service = createGarminWorkoutService(httpClient, options?.logger);
487
+ return { auth, service };
488
+ };
489
+ var DEFAULT_PATH = join(homedir(), ".kaiord", "garmin-tokens.json");
490
+ var createFileTokenStore = (filePath = DEFAULT_PATH) => ({
491
+ save: async (tokens) => {
492
+ await mkdir(dirname(filePath), { recursive: true });
493
+ await writeFile(filePath, JSON.stringify(tokens, null, 2), {
494
+ encoding: "utf-8",
495
+ mode: 384
496
+ });
497
+ },
498
+ load: async () => {
499
+ try {
500
+ const content = await readFile(filePath, "utf-8");
501
+ return JSON.parse(content);
502
+ } catch {
503
+ return null;
504
+ }
505
+ },
506
+ clear: async () => {
507
+ try {
508
+ await unlink(filePath);
509
+ } catch {
510
+ }
511
+ }
512
+ });
513
+
514
+ // src/adapters/token-store/memory-token-store.ts
515
+ var createMemoryTokenStore = () => {
516
+ let stored = null;
517
+ return {
518
+ save: async (tokens) => {
519
+ stored = tokens;
520
+ },
521
+ load: async () => stored,
522
+ clear: async () => {
523
+ stored = null;
524
+ }
525
+ };
526
+ };
527
+
528
+ export { createCookieFetch, createFileTokenStore, createGarminAuthProvider, createGarminConnectClient, createMemoryTokenStore };
529
+ //# sourceMappingURL=index.js.map
530
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/http/urls.ts","../src/adapters/http/oauth-consumer.ts","../src/adapters/http/sso-validators.ts","../src/adapters/http/sso-login.ts","../src/adapters/http/oauth-signer.ts","../src/adapters/http/sso-oauth.ts","../src/adapters/http/garmin-sso.ts","../src/adapters/http/token-refresh.ts","../src/adapters/http/garmin-http-client.ts","../src/adapters/http/cookie-fetch.ts","../src/adapters/schemas/garmin-token.schema.ts","../src/adapters/auth/garmin-auth-provider.ts","../src/adapters/mappers/workout-summary.mapper.ts","../src/adapters/schemas/workout-response.schema.ts","../src/adapters/client/garmin-workout-service.ts","../src/adapters/client/garmin-connect-client.ts","../src/adapters/token-store/file-token-store.ts","../src/adapters/token-store/memory-token-store.ts"],"names":["createServiceAuthError","createServiceApiError","z","createConsoleLogger"],"mappings":"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","file":"index.js","sourcesContent":["export const GARMIN_SSO_ORIGIN = \"https://sso.garmin.com\";\nexport const GARMIN_SSO_EMBED = \"https://sso.garmin.com/sso/embed\";\nexport const SIGNIN_URL = \"https://sso.garmin.com/sso/signin\";\nexport const GC_MODERN = \"https://connect.garmin.com/modern\";\nexport const API_BASE = \"https://connectapi.garmin.com\";\nexport const OAUTH_URL = `${API_BASE}/oauth-service/oauth`;\nexport const WORKOUT_URL = `${API_BASE}/workout-service`;\n/** OAuth consumer credentials hosted by the garth project (third-party). */\nexport const OAUTH_CONSUMER_URL =\n \"https://thegarth.s3.amazonaws.com/oauth_consumer.json\";\n\nexport const USER_AGENT_MOBILE = \"com.garmin.android.apps.connectmobile\";\nexport const USER_AGENT_BROWSER =\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36\";\n","import type { FetchFn, OAuthConsumer } from \"./types\";\nimport { createServiceAuthError } from \"@kaiord/core\";\nimport { OAUTH_CONSUMER_URL } from \"./urls\";\n\nexport const fetchOAuthConsumer = async (\n fetchFn: FetchFn\n): Promise<OAuthConsumer> => {\n const res = await fetchFn(OAUTH_CONSUMER_URL);\n if (!res.ok) {\n throw createServiceAuthError(\n `Failed to fetch OAuth consumer: ${res.status} ${res.statusText}`\n );\n }\n const data = (await res.json()) as {\n consumer_key: string;\n consumer_secret: string;\n };\n return { key: data.consumer_key, secret: data.consumer_secret };\n};\n","import type { Logger } from \"@kaiord/core\";\nimport { createServiceAuthError } from \"@kaiord/core\";\n\nconst ACCOUNT_LOCKED_RE = /var status\\s*=\\s*\"([^\"]*)\"/;\nconst PAGE_TITLE_RE = /<title>([^<]*)<\\/title>/;\n\nexport const checkAccountLocked = (html: string): void => {\n const match = ACCOUNT_LOCKED_RE.exec(html);\n if (match && match[1] === \"ACCOUNT_LOCKED\") {\n throw createServiceAuthError(\n `Account locked: ${match[1]}. Unlock via Garmin Connect web.`\n );\n }\n};\n\nexport const checkPageTitle = (html: string, logger: Logger): void => {\n const match = PAGE_TITLE_RE.exec(html);\n if (match?.[1]?.includes(\"Update Phone Number\")) {\n throw createServiceAuthError(\"Login failed: phone number update required.\");\n }\n if (match) {\n logger.debug(\"Login page title\", { title: match[1] });\n }\n};\n","import type { Logger } from \"@kaiord/core\";\nimport { createServiceAuthError } from \"@kaiord/core\";\nimport type { FetchFn } from \"./types\";\nimport {\n GARMIN_SSO_EMBED,\n GC_MODERN,\n GARMIN_SSO_ORIGIN,\n SIGNIN_URL,\n USER_AGENT_BROWSER,\n} from \"./urls\";\nimport { checkAccountLocked, checkPageTitle } from \"./sso-validators\";\n\nconst CSRF_RE = /name=\"_csrf\"\\s+value=\"(.+?)\"/;\nconst TICKET_RE = /ticket=([^\"]+)\"/;\n\nconst fetchCsrfToken = async (fetchFn: FetchFn): Promise<string> => {\n const signinParams = new URLSearchParams({\n id: \"gauth-widget\",\n embedWidget: \"true\",\n locale: \"en\",\n gauthHost: GARMIN_SSO_EMBED,\n });\n const csrfRes = await fetchFn(`${SIGNIN_URL}?${signinParams}`);\n if (!csrfRes.ok) {\n throw createServiceAuthError(\n `SSO login page returned ${csrfRes.status}: ${csrfRes.statusText}`\n );\n }\n const csrfHtml = await csrfRes.text();\n const csrfMatch = CSRF_RE.exec(csrfHtml);\n if (!csrfMatch) {\n throw createServiceAuthError(\"CSRF token not found on login page\");\n }\n return csrfMatch[1];\n};\n\nconst submitLogin = async (\n username: string,\n password: string,\n csrf: string,\n fetchFn: FetchFn\n): Promise<string> => {\n const loginParams = new URLSearchParams({\n id: \"gauth-widget\",\n embedWidget: \"true\",\n clientId: \"GarminConnect\",\n locale: \"en\",\n gauthHost: GARMIN_SSO_EMBED,\n service: GARMIN_SSO_EMBED,\n source: GARMIN_SSO_EMBED,\n redirectAfterAccountLoginUrl: GARMIN_SSO_EMBED,\n redirectAfterAccountCreationUrl: GARMIN_SSO_EMBED,\n });\n const body = new URLSearchParams({\n username,\n password,\n embed: \"true\",\n _csrf: csrf,\n });\n const loginRes = await fetchFn(`${SIGNIN_URL}?${loginParams}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Dnt: \"1\",\n Origin: GARMIN_SSO_ORIGIN,\n Referer: SIGNIN_URL,\n \"User-Agent\": USER_AGENT_BROWSER,\n },\n body: body.toString(),\n });\n return loginRes.text();\n};\n\nexport const getLoginTicket = async (\n username: string,\n password: string,\n fetchFn: FetchFn,\n logger: Logger\n): Promise<string> => {\n const embedParams = new URLSearchParams({\n clientId: \"GarminConnect\",\n locale: \"en\",\n service: GC_MODERN,\n });\n await fetchFn(`${GARMIN_SSO_EMBED}?${embedParams}`);\n\n const csrf = await fetchCsrfToken(fetchFn);\n const loginHtml = await submitLogin(username, password, csrf, fetchFn);\n\n checkAccountLocked(loginHtml);\n checkPageTitle(loginHtml, logger);\n\n const ticketMatch = TICKET_RE.exec(loginHtml);\n if (!ticketMatch) {\n throw createServiceAuthError(\n \"Login failed: ticket not found. Check username and password.\"\n );\n }\n return ticketMatch[1];\n};\n","import { createHmac } from \"node:crypto\";\nimport OAuth from \"oauth-1.0a\";\nimport type { OAuthConsumer } from \"./types\";\n\nexport type { OAuthConsumer } from \"./types\";\nexport type OAuthToken = { key: string; secret: string };\n\nexport type OAuthSigner = {\n toHeader: (\n request: { url: string; method: string },\n token?: OAuthToken\n ) => Record<string, string>;\n};\n\nexport const createOAuthSigner = (consumer: OAuthConsumer): OAuthSigner => {\n const oauth = new OAuth({\n consumer,\n signature_method: \"HMAC-SHA1\",\n hash_function(baseString: string, key: string) {\n return createHmac(\"sha1\", key).update(baseString).digest(\"base64\");\n },\n });\n\n return {\n toHeader: (request, token) => {\n const authorized = token\n ? oauth.authorize(request, token)\n : oauth.authorize(request);\n const header = oauth.toHeader(authorized);\n return Object.fromEntries(Object.entries(header));\n },\n };\n};\n","import { createServiceAuthError } from \"@kaiord/core\";\nimport type { FetchFn, OAuthConsumer } from \"./types\";\nimport { createOAuthSigner } from \"./oauth-signer\";\nimport { GARMIN_SSO_EMBED, OAUTH_URL, USER_AGENT_MOBILE } from \"./urls\";\nimport type { OAuth1Token, OAuth2Token } from \"./types\";\n\nexport const getOAuth1Token = async (\n ticket: string,\n consumer: OAuthConsumer,\n fetchFn: FetchFn\n): Promise<OAuth1Token> => {\n const signer = createOAuthSigner(consumer);\n const params = new URLSearchParams({\n ticket,\n \"login-url\": GARMIN_SSO_EMBED,\n \"accepts-mfa-tokens\": \"true\",\n });\n const url = `${OAUTH_URL}/preauthorized?${params}`;\n const headers = signer.toHeader({ url, method: \"GET\" });\n\n const res = await fetchFn(url, {\n headers: { ...headers, \"User-Agent\": USER_AGENT_MOBILE },\n });\n if (!res.ok) {\n throw createServiceAuthError(\n `OAuth1 token request failed: ${res.status} ${res.statusText}`\n );\n }\n const text = await res.text();\n const parsed = new URLSearchParams(text);\n\n const oauthToken = parsed.get(\"oauth_token\");\n const oauthTokenSecret = parsed.get(\"oauth_token_secret\");\n if (!oauthToken || !oauthTokenSecret) {\n throw createServiceAuthError(\"OAuth1 token exchange failed\");\n }\n\n return { oauth_token: oauthToken, oauth_token_secret: oauthTokenSecret };\n};\n\nexport const exchangeOAuth2 = async (\n oauth1: OAuth1Token,\n consumer: OAuthConsumer,\n fetchFn: FetchFn\n): Promise<OAuth2Token> => {\n const signer = createOAuthSigner(consumer);\n const baseUrl = `${OAUTH_URL}/exchange/user/2.0`;\n const token = {\n key: oauth1.oauth_token,\n secret: oauth1.oauth_token_secret,\n };\n const authHeader = signer.toHeader({ url: baseUrl, method: \"POST\" }, token);\n\n const res = await fetchFn(baseUrl, {\n method: \"POST\",\n headers: {\n ...authHeader,\n \"User-Agent\": USER_AGENT_MOBILE,\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n });\n if (!res.ok) {\n throw createServiceAuthError(\n `OAuth2 exchange failed: ${res.status} ${res.statusText}`\n );\n }\n const data = (await res.json()) as OAuth2Token;\n\n return {\n ...data,\n expires_at: Math.floor(Date.now() / 1000) + data.expires_in,\n };\n};\n","import type { Logger } from \"@kaiord/core\";\nimport type { FetchFn, OAuth1Token, OAuth2Token } from \"./types\";\nimport { fetchOAuthConsumer } from \"./oauth-consumer\";\nimport { getLoginTicket } from \"./sso-login\";\nimport { getOAuth1Token, exchangeOAuth2 as exchange } from \"./sso-oauth\";\n\nexport type { OAuth1Token, OAuth2Token } from \"./types\";\nexport type SsoResult = { oauth1: OAuth1Token; oauth2: OAuth2Token };\n\n/**\n * Garmin Connect SSO login flow.\n * The fetchFn must be cookie-aware (persist cookies across requests).\n * Use fetch-cookie or similar wrapper for Node.js environments.\n */\nexport const garminSso = async (\n username: string,\n password: string,\n logger: Logger,\n fetchFn: FetchFn\n): Promise<SsoResult> => {\n logger.info(\"Starting Garmin Connect SSO login\");\n\n const consumer = await fetchOAuthConsumer(fetchFn);\n const ticket = await getLoginTicket(username, password, fetchFn, logger);\n logger.debug(\"SSO ticket obtained\");\n\n const oauth1 = await getOAuth1Token(ticket, consumer, fetchFn);\n logger.debug(\"OAuth1 token obtained\");\n\n const oauth2 = await exchange(oauth1, consumer, fetchFn);\n logger.info(\"Garmin Connect SSO login successful\");\n\n return { oauth1, oauth2 };\n};\n\nexport { exchangeOAuth2 } from \"./sso-oauth\";\n","import type { Logger } from \"@kaiord/core\";\nimport { createServiceApiError } from \"@kaiord/core\";\nimport type { FetchFn, OAuthConsumer, OAuth1Token, OAuth2Token } from \"./types\";\nimport { exchangeOAuth2 } from \"./garmin-sso\";\nimport { fetchOAuthConsumer } from \"./oauth-consumer\";\n\ntype RefreshSubscriber = {\n resolve: (token: string) => void;\n reject: (error: unknown) => void;\n};\n\nexport type TokenState = {\n oauth1Token: OAuth1Token | undefined;\n oauth2Token: OAuth2Token | undefined;\n consumer: OAuthConsumer | undefined;\n};\n\nexport type TokenRefreshManager = {\n ensureFreshToken: () => Promise<void>;\n state: TokenState;\n};\n\nexport const createTokenRefreshManager = (\n fetchFn: FetchFn,\n logger: Logger\n): TokenRefreshManager => {\n const state: TokenState = {\n oauth1Token: undefined,\n oauth2Token: undefined,\n consumer: undefined,\n };\n let isRefreshing = false;\n let subscribers: RefreshSubscriber[] = [];\n\n const getConsumer = async (): Promise<OAuthConsumer> => {\n if (state.consumer) return state.consumer;\n state.consumer = await fetchOAuthConsumer(fetchFn);\n return state.consumer;\n };\n\n const refreshToken = async (): Promise<void> => {\n if (!state.oauth1Token || !state.oauth2Token) {\n throw createServiceApiError(\"No tokens available for refresh\", 401);\n }\n const cons = await getConsumer();\n state.oauth2Token = await exchangeOAuth2(state.oauth1Token, cons, fetchFn);\n logger.info(\"OAuth2 token refreshed\");\n };\n\n const waitForRefresh = (): Promise<string> =>\n new Promise((resolve, reject) => {\n subscribers.push({ resolve, reject });\n });\n\n const notifySubscribers = (): void => {\n if (!state.oauth2Token) return;\n const token = state.oauth2Token.access_token;\n subscribers.forEach((s) => {\n s.resolve(token);\n });\n subscribers = [];\n };\n\n const rejectSubscribers = (error: unknown): void => {\n subscribers.forEach((s) => {\n s.reject(error);\n });\n subscribers = [];\n };\n\n return {\n state,\n ensureFreshToken: async (): Promise<void> => {\n if (isRefreshing) {\n await waitForRefresh();\n return;\n }\n isRefreshing = true;\n try {\n await refreshToken();\n notifySubscribers();\n } catch (error) {\n rejectSubscribers(error);\n throw error;\n } finally {\n isRefreshing = false;\n }\n },\n };\n};\n","import type { Logger } from \"@kaiord/core\";\nimport { createServiceApiError } from \"@kaiord/core\";\nimport type {\n FetchFn,\n GarminHttpClient,\n OAuth1Token,\n OAuth2Token,\n} from \"./types\";\nimport { createTokenRefreshManager } from \"./token-refresh\";\n\nexport type { GarminHttpClient } from \"./types\";\n\nexport const createGarminHttpClient = (\n logger: Logger,\n fetchFn: FetchFn = globalThis.fetch\n): GarminHttpClient => {\n const refresh = createTokenRefreshManager(fetchFn, logger);\n\n const makeRequest = (url: string, init?: RequestInit): Promise<Response> => {\n if (!refresh.state.oauth2Token) {\n throw createServiceApiError(\"Token unavailable after refresh\", 401);\n }\n return fetchFn(url, {\n ...init,\n headers: {\n ...init?.headers,\n Authorization: `Bearer ${refresh.state.oauth2Token.access_token}`,\n },\n });\n };\n\n const authFetch = async (\n url: string,\n init?: RequestInit\n ): Promise<Response> => {\n if (!refresh.state.oauth2Token) {\n throw createServiceApiError(\"Not authenticated\", 401);\n }\n if (refresh.state.oauth2Token.expires_at < Math.floor(Date.now() / 1000)) {\n await refresh.ensureFreshToken();\n }\n\n const res = await makeRequest(url, init);\n\n if (res.status === 401) {\n await refresh.ensureFreshToken();\n const retry = await makeRequest(url, init);\n if (!retry.ok) {\n throw createServiceApiError(\n `API request failed after token refresh: ${retry.statusText}`,\n retry.status\n );\n }\n return retry;\n }\n if (!res.ok) {\n throw createServiceApiError(\n `API request failed: ${res.statusText}`,\n res.status\n );\n }\n return res;\n };\n\n return {\n get: async <T>(url: string): Promise<T> => {\n const res = await authFetch(url);\n return (await res.json()) as T;\n },\n post: async <T>(url: string, body: unknown): Promise<T> => {\n const res = await authFetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: body !== null ? JSON.stringify(body) : undefined,\n });\n return (await res.json()) as T;\n },\n del: async <T>(url: string): Promise<T> => {\n const res = await authFetch(url, {\n method: \"POST\",\n headers: { \"X-Http-Method-Override\": \"DELETE\" },\n });\n const text = await res.text();\n return (text ? JSON.parse(text) : undefined) as T;\n },\n setTokens: (o1: OAuth1Token, o2: OAuth2Token): void => {\n refresh.state.oauth1Token = o1;\n refresh.state.oauth2Token = o2;\n },\n clearTokens: (): void => {\n refresh.state.oauth1Token = undefined;\n refresh.state.oauth2Token = undefined;\n refresh.state.consumer = undefined;\n },\n getOAuth2Token: () => refresh.state.oauth2Token,\n };\n};\n","import fetchCookie from \"fetch-cookie\";\n\nexport const createCookieFetch = (): typeof globalThis.fetch =>\n fetchCookie(globalThis.fetch) as typeof globalThis.fetch;\n","import { z } from \"zod\";\n\nexport const oauth1TokenSchema = z.object({\n oauth_token: z.string(),\n oauth_token_secret: z.string(),\n});\n\nexport const oauth2TokenSchema = z.object({\n access_token: z.string(),\n refresh_token: z.string(),\n token_type: z.string(),\n expires_in: z.number(),\n refresh_token_expires_in: z.number(),\n expires_at: z.number(),\n});\n\nexport const garminTokensSchema = z.object({\n oauth1: oauth1TokenSchema,\n oauth2: oauth2TokenSchema,\n});\n","import type { AuthProvider, Logger, TokenStore } from \"@kaiord/core\";\nimport { createConsoleLogger, createServiceAuthError } from \"@kaiord/core\";\nimport type { GarminHttpClient } from \"../http/garmin-http-client\";\nimport { createGarminHttpClient } from \"../http/garmin-http-client\";\nimport { createCookieFetch } from \"../http/cookie-fetch\";\nimport { garminSso } from \"../http/garmin-sso\";\nimport { garminTokensSchema } from \"../schemas/garmin-token.schema\";\n\nexport type GarminAuthProviderOptions = {\n logger?: Logger;\n tokenStore?: TokenStore;\n fetchFn?: typeof globalThis.fetch;\n};\n\nexport type GarminAuthProviderResult = {\n auth: AuthProvider;\n httpClient: GarminHttpClient;\n};\n\nexport const createGarminAuthProvider = (\n options?: GarminAuthProviderOptions\n): GarminAuthProviderResult => {\n const logger = options?.logger ?? createConsoleLogger();\n const tokenStore = options?.tokenStore;\n const fetchFn = options?.fetchFn ?? createCookieFetch();\n const httpClient = createGarminHttpClient(logger, fetchFn);\n let currentOAuth1:\n | { oauth_token: string; oauth_token_secret: string }\n | undefined;\n\n const auth: AuthProvider = {\n login: async (username, password) => {\n const result = await garminSso(username, password, logger, fetchFn);\n currentOAuth1 = result.oauth1;\n httpClient.setTokens(result.oauth1, result.oauth2);\n if (tokenStore) {\n await tokenStore.save({\n oauth1: result.oauth1,\n oauth2: result.oauth2,\n });\n }\n },\n\n is_authenticated: () => {\n const token = httpClient.getOAuth2Token();\n if (!token) return false;\n return token.expires_at > Math.floor(Date.now() / 1000);\n },\n\n export_tokens: async () => {\n const token = httpClient.getOAuth2Token();\n if (!token || !currentOAuth1) {\n throw createServiceAuthError(\"No tokens to export\");\n }\n return { oauth1: currentOAuth1, oauth2: token };\n },\n\n restore_tokens: async (data) => {\n const parsed = garminTokensSchema.parse(data);\n currentOAuth1 = parsed.oauth1;\n httpClient.setTokens(parsed.oauth1, parsed.oauth2);\n logger.info(\"Tokens restored from stored session\");\n },\n\n logout: async () => {\n currentOAuth1 = undefined;\n httpClient.clearTokens();\n if (tokenStore) {\n await tokenStore.clear();\n }\n logger.info(\"Logged out from Garmin Connect\");\n },\n };\n\n return { auth, httpClient };\n};\n","import type { WorkoutSummary } from \"@kaiord/core\";\n\nexport const mapToWorkoutSummary = (garminWorkout: {\n workoutId?: number | string;\n workoutName?: string;\n sportType?: { sportTypeKey?: string };\n createdDate?: number | string;\n updatedDate?: number | string;\n}): WorkoutSummary => ({\n id: String(garminWorkout.workoutId ?? \"\"),\n name: garminWorkout.workoutName ?? \"Unnamed\",\n sport: garminWorkout.sportType?.sportTypeKey ?? \"unknown\",\n created_at: garminWorkout.createdDate\n ? new Date(garminWorkout.createdDate).toISOString()\n : \"\",\n updated_at: garminWorkout.updatedDate\n ? new Date(garminWorkout.updatedDate).toISOString()\n : \"\",\n});\n","import { z } from \"zod\";\n\n/** GET /workout-service/workouts?start=N&limit=N - List workout summaries */\nexport const garminWorkoutSummarySchema = z.object({\n workoutId: z.number().or(z.string()),\n workoutName: z.string().optional(),\n sportType: z.object({ sportTypeKey: z.string().optional() }).optional(),\n createdDate: z.number().or(z.string()).optional(),\n updatedDate: z.number().or(z.string()).optional(),\n});\n\n/** POST /workout-service/workout - Push a workout (returns created workout) */\nexport const garminPushResponseSchema = z.object({\n workoutId: z.number().or(z.string()),\n workoutName: z.string().optional(),\n});\n","import type {\n KRD,\n ListOptions,\n Logger,\n PushResult,\n WorkoutService,\n WorkoutSummary,\n} from \"@kaiord/core\";\nimport {\n createConsoleLogger,\n toText,\n createServiceApiError,\n} from \"@kaiord/core\";\nimport { createGarminWriter } from \"@kaiord/garmin\";\nimport type { GarminHttpClient } from \"../http/garmin-http-client\";\nimport { mapToWorkoutSummary } from \"../mappers/workout-summary.mapper\";\nimport {\n garminPushResponseSchema,\n garminWorkoutSummarySchema,\n} from \"../schemas/workout-response.schema\";\nimport { WORKOUT_URL } from \"../http/urls\";\n\nexport type GarminWorkoutClient = Pick<WorkoutService, \"push\" | \"list\">;\n\nconst pushWorkout = async (\n krd: KRD,\n httpClient: GarminHttpClient,\n garminWriter: ReturnType<typeof createGarminWriter>,\n log: Logger\n): Promise<PushResult> => {\n try {\n log.info(\"Pushing workout to Garmin Connect\");\n const gcnJson = await toText(krd, garminWriter, log);\n const payload = JSON.parse(gcnJson) as Record<string, unknown>;\n\n const raw = await httpClient.post<unknown>(\n `${WORKOUT_URL}/workout`,\n payload\n );\n const result = garminPushResponseSchema.parse(raw);\n\n return {\n id: String(result.workoutId),\n name: result.workoutName ?? \"Workout\",\n url: `https://connect.garmin.com/modern/workout/${result.workoutId}`,\n };\n } catch (error) {\n throw createServiceApiError(\"Failed to push workout\", undefined, error);\n }\n};\n\nconst listWorkouts = async (\n httpClient: GarminHttpClient,\n log: Logger,\n options?: ListOptions\n): Promise<WorkoutSummary[]> => {\n try {\n log.info(\"Listing workouts from Garmin Connect\");\n const start = options?.offset ?? 0;\n const limit = options?.limit ?? 20;\n const params = new URLSearchParams({\n start: String(start),\n limit: String(limit),\n });\n\n const raw = await httpClient.get<unknown>(\n `${WORKOUT_URL}/workouts?${params}`\n );\n const workouts = garminWorkoutSummarySchema.array().parse(raw);\n return workouts.map(mapToWorkoutSummary);\n } catch (error) {\n throw createServiceApiError(\"Failed to list workouts\", undefined, error);\n }\n};\n\nexport const createGarminWorkoutService = (\n httpClient: GarminHttpClient,\n logger?: Logger\n): GarminWorkoutClient => {\n const log = logger ?? createConsoleLogger();\n const garminWriter = createGarminWriter(log);\n\n return {\n push: (krd) => pushWorkout(krd, httpClient, garminWriter, log),\n list: (opts) => listWorkouts(httpClient, log, opts),\n };\n};\n","import type { GarminAuthProviderOptions } from \"../auth/garmin-auth-provider\";\nimport { createGarminAuthProvider } from \"../auth/garmin-auth-provider\";\nimport { createGarminWorkoutService } from \"./garmin-workout-service\";\n\nexport type GarminConnectClientOptions = GarminAuthProviderOptions;\n\nexport const createGarminConnectClient = (\n options?: GarminConnectClientOptions\n) => {\n const { auth, httpClient } = createGarminAuthProvider(options);\n const service = createGarminWorkoutService(httpClient, options?.logger);\n return { auth, service };\n};\n","import { readFile, writeFile, mkdir, unlink } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { TokenData, TokenStore } from \"@kaiord/core\";\n\nconst DEFAULT_PATH = join(homedir(), \".kaiord\", \"garmin-tokens.json\");\n\nexport const createFileTokenStore = (\n filePath: string = DEFAULT_PATH\n): TokenStore => ({\n save: async (tokens: TokenData) => {\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(filePath, JSON.stringify(tokens, null, 2), {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n },\n\n load: async (): Promise<TokenData | null> => {\n try {\n const content = await readFile(filePath, \"utf-8\");\n return JSON.parse(content) as TokenData;\n } catch {\n return null;\n }\n },\n\n clear: async () => {\n try {\n await unlink(filePath);\n } catch {\n // File may not exist\n }\n },\n});\n","import type { TokenData, TokenStore } from \"@kaiord/core\";\n\nexport const createMemoryTokenStore = (): TokenStore => {\n let stored: TokenData | null = null;\n\n return {\n save: async (tokens: TokenData) => {\n stored = tokens;\n },\n load: async () => stored,\n clear: async () => {\n stored = null;\n },\n };\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@kaiord/garmin-connect",
3
+ "version": "0.1.0",
4
+ "description": "Garmin Connect API client for the Kaiord health & fitness data framework",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "test": "vitest --run",
21
+ "test:coverage": "vitest --run --coverage",
22
+ "test:integration": "vitest --run --config vitest.integration.config.ts",
23
+ "test:watch": "vitest",
24
+ "lint": "tsc --noEmit && eslint . && prettier --check src",
25
+ "lint:fix": "eslint . --fix && prettier --write src",
26
+ "clean": "rm -rf dist"
27
+ },
28
+ "dependencies": {
29
+ "@kaiord/core": "workspace:^",
30
+ "@kaiord/garmin": "workspace:^",
31
+ "fetch-cookie": "^3.2.0",
32
+ "oauth-1.0a": "^2.2.6",
33
+ "zod": "^4.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^24.0.0",
37
+ "@vitest/coverage-v8": "^4.0.18",
38
+ "tsup": "^8.5.1",
39
+ "tsx": "^4.7.0",
40
+ "typescript": "^5.3.3",
41
+ "vitest": "^4.0.18"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/pablo-albaladejo/kaiord.git",
46
+ "directory": "packages/garmin-connect"
47
+ },
48
+ "homepage": "https://www.npmjs.com/package/@kaiord/garmin-connect",
49
+ "bugs": {
50
+ "url": "https://github.com/pablo-albaladejo/kaiord/issues"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "keywords": [
56
+ "kaiord",
57
+ "garmin",
58
+ "garmin-connect",
59
+ "workout",
60
+ "api-client",
61
+ "fitness"
62
+ ],
63
+ "author": "Kaiord Contributors",
64
+ "license": "MIT"
65
+ }