@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 +114 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +530 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @kaiord/garmin-connect
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@kaiord/garmin-connect)
|
|
4
|
+
[](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
|
package/dist/index.d.ts
ADDED
|
@@ -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":";;;;;;;;;;;;;AAAO,IAAM,iBAAA,GAAoB,wBAAA;AAC1B,IAAM,gBAAA,GAAmB,kCAAA;AACzB,IAAM,UAAA,GAAa,mCAAA;AACnB,IAAM,SAAA,GAAY,mCAAA;AAClB,IAAM,QAAA,GAAW,+BAAA;AACjB,IAAM,SAAA,GAAY,GAAG,QAAQ,CAAA,oBAAA,CAAA;AAC7B,IAAM,WAAA,GAAc,GAAG,QAAQ,CAAA,gBAAA,CAAA;AAE/B,IAAM,kBAAA,GACX,uDAAA;AAEK,IAAM,iBAAA,GAAoB,uCAAA;AAC1B,IAAM,kBAAA,GACX,iHAAA;;;ACTK,IAAM,kBAAA,GAAqB,OAChC,OAAA,KAC2B;AAC3B,EAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,kBAAkB,CAAA;AAC5C,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,IAAA,MAAM,sBAAA;AAAA,MACJ,CAAA,gCAAA,EAAmC,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,IAAI,UAAU,CAAA;AAAA,KACjE;AAAA,EACF;AACA,EAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAI7B,EAAA,OAAO,EAAE,GAAA,EAAK,IAAA,CAAK,YAAA,EAAc,MAAA,EAAQ,KAAK,eAAA,EAAgB;AAChE,CAAA;ACfA,IAAM,iBAAA,GAAoB,4BAAA;AAC1B,IAAM,aAAA,GAAgB,yBAAA;AAEf,IAAM,kBAAA,GAAqB,CAAC,IAAA,KAAuB;AACxD,EAAA,MAAM,KAAA,GAAQ,iBAAA,CAAkB,IAAA,CAAK,IAAI,CAAA;AACzC,EAAA,IAAI,KAAA,IAAS,KAAA,CAAM,CAAC,CAAA,KAAM,gBAAA,EAAkB;AAC1C,IAAA,MAAMA,sBAAAA;AAAA,MACJ,CAAA,gBAAA,EAAmB,KAAA,CAAM,CAAC,CAAC,CAAA,gCAAA;AAAA,KAC7B;AAAA,EACF;AACF,CAAA;AAEO,IAAM,cAAA,GAAiB,CAAC,IAAA,EAAc,MAAA,KAAyB;AACpE,EAAA,MAAM,KAAA,GAAQ,aAAA,CAAc,IAAA,CAAK,IAAI,CAAA;AACrC,EAAA,IAAI,KAAA,GAAQ,CAAC,CAAA,EAAG,QAAA,CAAS,qBAAqB,CAAA,EAAG;AAC/C,IAAA,MAAMA,uBAAuB,6CAA6C,CAAA;AAAA,EAC5E;AACA,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,MAAA,CAAO,MAAM,kBAAA,EAAoB,EAAE,OAAO,KAAA,CAAM,CAAC,GAAG,CAAA;AAAA,EACtD;AACF,CAAA;;;ACXA,IAAM,OAAA,GAAU,8BAAA;AAChB,IAAM,SAAA,GAAY,iBAAA;AAElB,IAAM,cAAA,GAAiB,OAAO,OAAA,KAAsC;AAClE,EAAA,MAAM,YAAA,GAAe,IAAI,eAAA,CAAgB;AAAA,IACvC,EAAA,EAAI,cAAA;AAAA,IACJ,WAAA,EAAa,MAAA;AAAA,IACb,MAAA,EAAQ,IAAA;AAAA,IACR,SAAA,EAAW;AAAA,GACZ,CAAA;AACD,EAAA,MAAM,UAAU,MAAM,OAAA,CAAQ,GAAG,UAAU,CAAA,CAAA,EAAI,YAAY,CAAA,CAAE,CAAA;AAC7D,EAAA,IAAI,CAAC,QAAQ,EAAA,EAAI;AACf,IAAA,MAAMA,sBAAAA;AAAA,MACJ,CAAA,wBAAA,EAA2B,OAAA,CAAQ,MAAM,CAAA,EAAA,EAAK,QAAQ,UAAU,CAAA;AAAA,KAClE;AAAA,EACF;AACA,EAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,IAAA,EAAK;AACpC,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,IAAA,CAAK,QAAQ,CAAA;AACvC,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAMA,uBAAuB,oCAAoC,CAAA;AAAA,EACnE;AACA,EAAA,OAAO,UAAU,CAAC,CAAA;AACpB,CAAA;AAEA,IAAM,WAAA,GAAc,OAClB,QAAA,EACA,QAAA,EACA,MACA,OAAA,KACoB;AACpB,EAAA,MAAM,WAAA,GAAc,IAAI,eAAA,CAAgB;AAAA,IACtC,EAAA,EAAI,cAAA;AAAA,IACJ,WAAA,EAAa,MAAA;AAAA,IACb,QAAA,EAAU,eAAA;AAAA,IACV,MAAA,EAAQ,IAAA;AAAA,IACR,SAAA,EAAW,gBAAA;AAAA,IACX,OAAA,EAAS,gBAAA;AAAA,IACT,MAAA,EAAQ,gBAAA;AAAA,IACR,4BAAA,EAA8B,gBAAA;AAAA,IAC9B,+BAAA,EAAiC;AAAA,GAClC,CAAA;AACD,EAAA,MAAM,IAAA,GAAO,IAAI,eAAA,CAAgB;AAAA,IAC/B,QAAA;AAAA,IACA,QAAA;AAAA,IACA,KAAA,EAAO,MAAA;AAAA,IACP,KAAA,EAAO;AAAA,GACR,CAAA;AACD,EAAA,MAAM,WAAW,MAAM,OAAA,CAAQ,GAAG,UAAU,CAAA,CAAA,EAAI,WAAW,CAAA,CAAA,EAAI;AAAA,IAC7D,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,mCAAA;AAAA,MAChB,GAAA,EAAK,GAAA;AAAA,MACL,MAAA,EAAQ,iBAAA;AAAA,MACR,OAAA,EAAS,UAAA;AAAA,MACT,YAAA,EAAc;AAAA,KAChB;AAAA,IACA,IAAA,EAAM,KAAK,QAAA;AAAS,GACrB,CAAA;AACD,EAAA,OAAO,SAAS,IAAA,EAAK;AACvB,CAAA;AAEO,IAAM,cAAA,GAAiB,OAC5B,QAAA,EACA,QAAA,EACA,SACA,MAAA,KACoB;AACpB,EAAA,MAAM,WAAA,GAAc,IAAI,eAAA,CAAgB;AAAA,IACtC,QAAA,EAAU,eAAA;AAAA,IACV,MAAA,EAAQ,IAAA;AAAA,IACR,OAAA,EAAS;AAAA,GACV,CAAA;AACD,EAAA,MAAM,OAAA,CAAQ,CAAA,EAAG,gBAAgB,CAAA,CAAA,EAAI,WAAW,CAAA,CAAE,CAAA;AAElD,EAAA,MAAM,IAAA,GAAO,MAAM,cAAA,CAAe,OAAO,CAAA;AACzC,EAAA,MAAM,YAAY,MAAM,WAAA,CAAY,QAAA,EAAU,QAAA,EAAU,MAAM,OAAO,CAAA;AAErE,EAAA,kBAAA,CAAmB,SAAS,CAAA;AAC5B,EAAA,cAAA,CAAe,WAAW,MAAM,CAAA;AAEhC,EAAA,MAAM,WAAA,GAAc,SAAA,CAAU,IAAA,CAAK,SAAS,CAAA;AAC5C,EAAA,IAAI,CAAC,WAAA,EAAa;AAChB,IAAA,MAAMA,sBAAAA;AAAA,MACJ;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,YAAY,CAAC,CAAA;AACtB,CAAA;ACrFO,IAAM,iBAAA,GAAoB,CAAC,QAAA,KAAyC;AACzE,EAAA,MAAM,KAAA,GAAQ,IAAI,KAAA,CAAM;AAAA,IACtB,QAAA;AAAA,IACA,gBAAA,EAAkB,WAAA;AAAA,IAClB,aAAA,CAAc,YAAoB,GAAA,EAAa;AAC7C,MAAA,OAAO,UAAA,CAAW,QAAQ,GAAG,CAAA,CAAE,OAAO,UAAU,CAAA,CAAE,OAAO,QAAQ,CAAA;AAAA,IACnE;AAAA,GACD,CAAA;AAED,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,CAAC,OAAA,EAAS,KAAA,KAAU;AAC5B,MAAA,MAAM,UAAA,GAAa,QACf,KAAA,CAAM,SAAA,CAAU,SAAS,KAAK,CAAA,GAC9B,KAAA,CAAM,SAAA,CAAU,OAAO,CAAA;AAC3B,MAAA,MAAM,MAAA,GAAS,KAAA,CAAM,QAAA,CAAS,UAAU,CAAA;AACxC,MAAA,OAAO,MAAA,CAAO,WAAA,CAAY,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAC,CAAA;AAAA,IAClD;AAAA,GACF;AACF,CAAA;;;AC1BO,IAAM,cAAA,GAAiB,OAC5B,MAAA,EACA,QAAA,EACA,OAAA,KACyB;AACzB,EAAA,MAAM,MAAA,GAAS,kBAAkB,QAAQ,CAAA;AACzC,EAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB;AAAA,IACjC,MAAA;AAAA,IACA,WAAA,EAAa,gBAAA;AAAA,IACb,oBAAA,EAAsB;AAAA,GACvB,CAAA;AACD,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,SAAS,CAAA,eAAA,EAAkB,MAAM,CAAA,CAAA;AAChD,EAAA,MAAM,UAAU,MAAA,CAAO,QAAA,CAAS,EAAE,GAAA,EAAK,MAAA,EAAQ,OAAO,CAAA;AAEtD,EAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,GAAA,EAAK;AAAA,IAC7B,OAAA,EAAS,EAAE,GAAG,OAAA,EAAS,cAAc,iBAAA;AAAkB,GACxD,CAAA;AACD,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,IAAA,MAAMA,sBAAAA;AAAA,MACJ,CAAA,6BAAA,EAAgC,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,IAAI,UAAU,CAAA;AAAA,KAC9D;AAAA,EACF;AACA,EAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,EAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,IAAI,CAAA;AAEvC,EAAA,MAAM,UAAA,GAAa,MAAA,CAAO,GAAA,CAAI,aAAa,CAAA;AAC3C,EAAA,MAAM,gBAAA,GAAmB,MAAA,CAAO,GAAA,CAAI,oBAAoB,CAAA;AACxD,EAAA,IAAI,CAAC,UAAA,IAAc,CAAC,gBAAA,EAAkB;AACpC,IAAA,MAAMA,uBAAuB,8BAA8B,CAAA;AAAA,EAC7D;AAEA,EAAA,OAAO,EAAE,WAAA,EAAa,UAAA,EAAY,kBAAA,EAAoB,gBAAA,EAAiB;AACzE,CAAA;AAEO,IAAM,cAAA,GAAiB,OAC5B,MAAA,EACA,QAAA,EACA,OAAA,KACyB;AACzB,EAAA,MAAM,MAAA,GAAS,kBAAkB,QAAQ,CAAA;AACzC,EAAA,MAAM,OAAA,GAAU,GAAG,SAAS,CAAA,kBAAA,CAAA;AAC5B,EAAA,MAAM,KAAA,GAAQ;AAAA,IACZ,KAAK,MAAA,CAAO,WAAA;AAAA,IACZ,QAAQ,MAAA,CAAO;AAAA,GACjB;AACA,EAAA,MAAM,UAAA,GAAa,OAAO,QAAA,CAAS,EAAE,KAAK,OAAA,EAAS,MAAA,EAAQ,MAAA,EAAO,EAAG,KAAK,CAAA;AAE1E,EAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,OAAA,EAAS;AAAA,IACjC,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,GAAG,UAAA;AAAA,MACH,YAAA,EAAc,iBAAA;AAAA,MACd,cAAA,EAAgB;AAAA;AAClB,GACD,CAAA;AACD,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,IAAA,MAAMA,sBAAAA;AAAA,MACJ,CAAA,wBAAA,EAA2B,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,IAAI,UAAU,CAAA;AAAA,KACzD;AAAA,EACF;AACA,EAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAE7B,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,UAAA,EAAY,KAAK,KAAA,CAAM,IAAA,CAAK,KAAI,GAAI,GAAI,IAAI,IAAA,CAAK;AAAA,GACnD;AACF,CAAA;;;AC1DO,IAAM,SAAA,GAAY,OACvB,QAAA,EACA,QAAA,EACA,QACA,OAAA,KACuB;AACvB,EAAA,MAAA,CAAO,KAAK,mCAAmC,CAAA;AAE/C,EAAA,MAAM,QAAA,GAAW,MAAM,kBAAA,CAAmB,OAAO,CAAA;AACjD,EAAA,MAAM,SAAS,MAAM,cAAA,CAAe,QAAA,EAAU,QAAA,EAAU,SAAS,MAAM,CAAA;AACvE,EAAA,MAAA,CAAO,MAAM,qBAAqB,CAAA;AAElC,EAAA,MAAM,MAAA,GAAS,MAAM,cAAA,CAAe,MAAA,EAAQ,UAAU,OAAO,CAAA;AAC7D,EAAA,MAAA,CAAO,MAAM,uBAAuB,CAAA;AAEpC,EAAA,MAAM,MAAA,GAAS,MAAM,cAAA,CAAS,MAAA,EAAQ,UAAU,OAAO,CAAA;AACvD,EAAA,MAAA,CAAO,KAAK,qCAAqC,CAAA;AAEjD,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC1B,CAAA;;;ACXO,IAAM,yBAAA,GAA4B,CACvC,OAAA,EACA,MAAA,KACwB;AACxB,EAAA,MAAM,KAAA,GAAoB;AAAA,IACxB,WAAA,EAAa,MAAA;AAAA,IACb,WAAA,EAAa,MAAA;AAAA,IACb,QAAA,EAAU;AAAA,GACZ;AACA,EAAA,IAAI,YAAA,GAAe,KAAA;AACnB,EAAA,IAAI,cAAmC,EAAC;AAExC,EAAA,MAAM,cAAc,YAAoC;AACtD,IAAA,IAAI,KAAA,CAAM,QAAA,EAAU,OAAO,KAAA,CAAM,QAAA;AACjC,IAAA,KAAA,CAAM,QAAA,GAAW,MAAM,kBAAA,CAAmB,OAAO,CAAA;AACjD,IAAA,OAAO,KAAA,CAAM,QAAA;AAAA,EACf,CAAA;AAEA,EAAA,MAAM,eAAe,YAA2B;AAC9C,IAAA,IAAI,CAAC,KAAA,CAAM,WAAA,IAAe,CAAC,MAAM,WAAA,EAAa;AAC5C,MAAA,MAAM,qBAAA,CAAsB,mCAAmC,GAAG,CAAA;AAAA,IACpE;AACA,IAAA,MAAM,IAAA,GAAO,MAAM,WAAA,EAAY;AAC/B,IAAA,KAAA,CAAM,cAAc,MAAM,cAAA,CAAe,KAAA,CAAM,WAAA,EAAa,MAAM,OAAO,CAAA;AACzE,IAAA,MAAA,CAAO,KAAK,wBAAwB,CAAA;AAAA,EACtC,CAAA;AAEA,EAAA,MAAM,iBAAiB,MACrB,IAAI,OAAA,CAAQ,CAAC,SAAS,MAAA,KAAW;AAC/B,IAAA,WAAA,CAAY,IAAA,CAAK,EAAE,OAAA,EAAS,MAAA,EAAQ,CAAA;AAAA,EACtC,CAAC,CAAA;AAEH,EAAA,MAAM,oBAAoB,MAAY;AACpC,IAAA,IAAI,CAAC,MAAM,WAAA,EAAa;AACxB,IAAA,MAAM,KAAA,GAAQ,MAAM,WAAA,CAAY,YAAA;AAChC,IAAA,WAAA,CAAY,OAAA,CAAQ,CAAC,CAAA,KAAM;AACzB,MAAA,CAAA,CAAE,QAAQ,KAAK,CAAA;AAAA,IACjB,CAAC,CAAA;AACD,IAAA,WAAA,GAAc,EAAC;AAAA,EACjB,CAAA;AAEA,EAAA,MAAM,iBAAA,GAAoB,CAAC,KAAA,KAAyB;AAClD,IAAA,WAAA,CAAY,OAAA,CAAQ,CAAC,CAAA,KAAM;AACzB,MAAA,CAAA,CAAE,OAAO,KAAK,CAAA;AAAA,IAChB,CAAC,CAAA;AACD,IAAA,WAAA,GAAc,EAAC;AAAA,EACjB,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,kBAAkB,YAA2B;AAC3C,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,cAAA,EAAe;AACrB,QAAA;AAAA,MACF;AACA,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,IAAI;AACF,QAAA,MAAM,YAAA,EAAa;AACnB,QAAA,iBAAA,EAAkB;AAAA,MACpB,SAAS,KAAA,EAAO;AACd,QAAA,iBAAA,CAAkB,KAAK,CAAA;AACvB,QAAA,MAAM,KAAA;AAAA,MACR,CAAA,SAAE;AACA,QAAA,YAAA,GAAe,KAAA;AAAA,MACjB;AAAA,IACF;AAAA,GACF;AACF,CAAA;;;AC7EO,IAAM,sBAAA,GAAyB,CACpC,MAAA,EACA,OAAA,GAAmB,WAAW,KAAA,KACT;AACrB,EAAA,MAAM,OAAA,GAAU,yBAAA,CAA0B,OAAA,EAAS,MAAM,CAAA;AAEzD,EAAA,MAAM,WAAA,GAAc,CAAC,GAAA,EAAa,IAAA,KAA0C;AAC1E,IAAA,IAAI,CAAC,OAAA,CAAQ,KAAA,CAAM,WAAA,EAAa;AAC9B,MAAA,MAAMC,qBAAAA,CAAsB,mCAAmC,GAAG,CAAA;AAAA,IACpE;AACA,IAAA,OAAO,QAAQ,GAAA,EAAK;AAAA,MAClB,GAAG,IAAA;AAAA,MACH,OAAA,EAAS;AAAA,QACP,GAAG,IAAA,EAAM,OAAA;AAAA,QACT,aAAA,EAAe,CAAA,OAAA,EAAU,OAAA,CAAQ,KAAA,CAAM,YAAY,YAAY,CAAA;AAAA;AACjE,KACD,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,MAAM,SAAA,GAAY,OAChB,GAAA,EACA,IAAA,KACsB;AACtB,IAAA,IAAI,CAAC,OAAA,CAAQ,KAAA,CAAM,WAAA,EAAa;AAC9B,MAAA,MAAMA,qBAAAA,CAAsB,qBAAqB,GAAG,CAAA;AAAA,IACtD;AACA,IAAA,IAAI,OAAA,CAAQ,KAAA,CAAM,WAAA,CAAY,UAAA,GAAa,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA,EAAI,GAAI,GAAI,CAAA,EAAG;AACxE,MAAA,MAAM,QAAQ,gBAAA,EAAiB;AAAA,IACjC;AAEA,IAAA,MAAM,GAAA,GAAM,MAAM,WAAA,CAAY,GAAA,EAAK,IAAI,CAAA;AAEvC,IAAA,IAAI,GAAA,CAAI,WAAW,GAAA,EAAK;AACtB,MAAA,MAAM,QAAQ,gBAAA,EAAiB;AAC/B,MAAA,MAAM,KAAA,GAAQ,MAAM,WAAA,CAAY,GAAA,EAAK,IAAI,CAAA;AACzC,MAAA,IAAI,CAAC,MAAM,EAAA,EAAI;AACb,QAAA,MAAMA,qBAAAA;AAAA,UACJ,CAAA,wCAAA,EAA2C,MAAM,UAAU,CAAA,CAAA;AAAA,UAC3D,KAAA,CAAM;AAAA,SACR;AAAA,MACF;AACA,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAMA,qBAAAA;AAAA,QACJ,CAAA,oBAAA,EAAuB,IAAI,UAAU,CAAA,CAAA;AAAA,QACrC,GAAA,CAAI;AAAA,OACN;AAAA,IACF;AACA,IAAA,OAAO,GAAA;AAAA,EACT,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,GAAA,EAAK,OAAU,GAAA,KAA4B;AACzC,MAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAG,CAAA;AAC/B,MAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,IACzB,CAAA;AAAA,IACA,IAAA,EAAM,OAAU,GAAA,EAAa,IAAA,KAA8B;AACzD,MAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAA,EAAK;AAAA,QAC/B,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,MAAM,IAAA,KAAS,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,GAAI;AAAA,OAC9C,CAAA;AACD,MAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,IACzB,CAAA;AAAA,IACA,GAAA,EAAK,OAAU,GAAA,KAA4B;AACzC,MAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAA,EAAK;AAAA,QAC/B,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,wBAAA,EAA0B,QAAA;AAAS,OAC/C,CAAA;AACD,MAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,MAAA,OAAQ,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,GAAI,MAAA;AAAA,IACpC,CAAA;AAAA,IACA,SAAA,EAAW,CAAC,EAAA,EAAiB,EAAA,KAA0B;AACrD,MAAA,OAAA,CAAQ,MAAM,WAAA,GAAc,EAAA;AAC5B,MAAA,OAAA,CAAQ,MAAM,WAAA,GAAc,EAAA;AAAA,IAC9B,CAAA;AAAA,IACA,aAAa,MAAY;AACvB,MAAA,OAAA,CAAQ,MAAM,WAAA,GAAc,MAAA;AAC5B,MAAA,OAAA,CAAQ,MAAM,WAAA,GAAc,MAAA;AAC5B,MAAA,OAAA,CAAQ,MAAM,QAAA,GAAW,MAAA;AAAA,IAC3B,CAAA;AAAA,IACA,cAAA,EAAgB,MAAM,OAAA,CAAQ,KAAA,CAAM;AAAA,GACtC;AACF,CAAA;AC9FO,IAAM,iBAAA,GAAoB,MAC/B,WAAA,CAAY,UAAA,CAAW,KAAK;ACDvB,IAAM,iBAAA,GAAoB,EAAE,MAAA,CAAO;AAAA,EACxC,WAAA,EAAa,EAAE,MAAA,EAAO;AAAA,EACtB,kBAAA,EAAoB,EAAE,MAAA;AACxB,CAAC,CAAA;AAEM,IAAM,iBAAA,GAAoB,EAAE,MAAA,CAAO;AAAA,EACxC,YAAA,EAAc,EAAE,MAAA,EAAO;AAAA,EACvB,aAAA,EAAe,EAAE,MAAA,EAAO;AAAA,EACxB,UAAA,EAAY,EAAE,MAAA,EAAO;AAAA,EACrB,UAAA,EAAY,EAAE,MAAA,EAAO;AAAA,EACrB,wBAAA,EAA0B,EAAE,MAAA,EAAO;AAAA,EACnC,UAAA,EAAY,EAAE,MAAA;AAChB,CAAC,CAAA;AAEM,IAAM,kBAAA,GAAqB,EAAE,MAAA,CAAO;AAAA,EACzC,MAAA,EAAQ,iBAAA;AAAA,EACR,MAAA,EAAQ;AACV,CAAC,CAAA;;;ACAM,IAAM,wBAAA,GAA2B,CACtC,OAAA,KAC6B;AAC7B,EAAA,MAAM,MAAA,GAAS,OAAA,EAAS,MAAA,IAAU,mBAAA,EAAoB;AACtD,EAAA,MAAM,aAAa,OAAA,EAAS,UAAA;AAC5B,EAAA,MAAM,OAAA,GAAU,OAAA,EAAS,OAAA,IAAW,iBAAA,EAAkB;AACtD,EAAA,MAAM,UAAA,GAAa,sBAAA,CAAuB,MAAA,EAAQ,OAAO,CAAA;AACzD,EAAA,IAAI,aAAA;AAIJ,EAAA,MAAM,IAAA,GAAqB;AAAA,IACzB,KAAA,EAAO,OAAO,QAAA,EAAU,QAAA,KAAa;AACnC,MAAA,MAAM,SAAS,MAAM,SAAA,CAAU,QAAA,EAAU,QAAA,EAAU,QAAQ,OAAO,CAAA;AAClE,MAAA,aAAA,GAAgB,MAAA,CAAO,MAAA;AACvB,MAAA,UAAA,CAAW,SAAA,CAAU,MAAA,CAAO,MAAA,EAAQ,MAAA,CAAO,MAAM,CAAA;AACjD,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,MAAM,WAAW,IAAA,CAAK;AAAA,UACpB,QAAQ,MAAA,CAAO,MAAA;AAAA,UACf,QAAQ,MAAA,CAAO;AAAA,SAChB,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AAAA,IAEA,kBAAkB,MAAM;AACtB,MAAA,MAAM,KAAA,GAAQ,WAAW,cAAA,EAAe;AACxC,MAAA,IAAI,CAAC,OAAO,OAAO,KAAA;AACnB,MAAA,OAAO,MAAM,UAAA,GAAa,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA,KAAQ,GAAI,CAAA;AAAA,IACxD,CAAA;AAAA,IAEA,eAAe,YAAY;AACzB,MAAA,MAAM,KAAA,GAAQ,WAAW,cAAA,EAAe;AACxC,MAAA,IAAI,CAAC,KAAA,IAAS,CAAC,aAAA,EAAe;AAC5B,QAAA,MAAMD,uBAAuB,qBAAqB,CAAA;AAAA,MACpD;AACA,MAAA,OAAO,EAAE,MAAA,EAAQ,aAAA,EAAe,MAAA,EAAQ,KAAA,EAAM;AAAA,IAChD,CAAA;AAAA,IAEA,cAAA,EAAgB,OAAO,IAAA,KAAS;AAC9B,MAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,KAAA,CAAM,IAAI,CAAA;AAC5C,MAAA,aAAA,GAAgB,MAAA,CAAO,MAAA;AACvB,MAAA,UAAA,CAAW,SAAA,CAAU,MAAA,CAAO,MAAA,EAAQ,MAAA,CAAO,MAAM,CAAA;AACjD,MAAA,MAAA,CAAO,KAAK,qCAAqC,CAAA;AAAA,IACnD,CAAA;AAAA,IAEA,QAAQ,YAAY;AAClB,MAAA,aAAA,GAAgB,MAAA;AAChB,MAAA,UAAA,CAAW,WAAA,EAAY;AACvB,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,MAAM,WAAW,KAAA,EAAM;AAAA,MACzB;AACA,MAAA,MAAA,CAAO,KAAK,gCAAgC,CAAA;AAAA,IAC9C;AAAA,GACF;AAEA,EAAA,OAAO,EAAE,MAAM,UAAA,EAAW;AAC5B;;;ACzEO,IAAM,mBAAA,GAAsB,CAAC,aAAA,MAMb;AAAA,EACrB,EAAA,EAAI,MAAA,CAAO,aAAA,CAAc,SAAA,IAAa,EAAE,CAAA;AAAA,EACxC,IAAA,EAAM,cAAc,WAAA,IAAe,SAAA;AAAA,EACnC,KAAA,EAAO,aAAA,CAAc,SAAA,EAAW,YAAA,IAAgB,SAAA;AAAA,EAChD,UAAA,EAAY,cAAc,WAAA,GACtB,IAAI,KAAK,aAAA,CAAc,WAAW,CAAA,CAAE,WAAA,EAAY,GAChD,EAAA;AAAA,EACJ,UAAA,EAAY,cAAc,WAAA,GACtB,IAAI,KAAK,aAAA,CAAc,WAAW,CAAA,CAAE,WAAA,EAAY,GAChD;AACN,CAAA,CAAA;ACfO,IAAM,0BAAA,GAA6BE,EAAE,MAAA,CAAO;AAAA,EACjD,WAAWA,CAAAA,CAAE,MAAA,GAAS,EAAA,CAAGA,CAAAA,CAAE,QAAQ,CAAA;AAAA,EACnC,WAAA,EAAaA,CAAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EACjC,SAAA,EAAWA,CAAAA,CAAE,MAAA,CAAO,EAAE,YAAA,EAAcA,CAAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS,EAAG,CAAA,CAAE,QAAA,EAAS;AAAA,EACtE,WAAA,EAAaA,EAAE,MAAA,EAAO,CAAE,GAAGA,CAAAA,CAAE,MAAA,EAAQ,CAAA,CAAE,QAAA,EAAS;AAAA,EAChD,WAAA,EAAaA,EAAE,MAAA,EAAO,CAAE,GAAGA,CAAAA,CAAE,MAAA,EAAQ,CAAA,CAAE,QAAA;AACzC,CAAC,CAAA;AAGM,IAAM,wBAAA,GAA2BA,EAAE,MAAA,CAAO;AAAA,EAC/C,WAAWA,CAAAA,CAAE,MAAA,GAAS,EAAA,CAAGA,CAAAA,CAAE,QAAQ,CAAA;AAAA,EACnC,WAAA,EAAaA,CAAAA,CAAE,MAAA,EAAO,CAAE,QAAA;AAC1B,CAAC,CAAA;;;ACSD,IAAM,WAAA,GAAc,OAClB,GAAA,EACA,UAAA,EACA,cACA,GAAA,KACwB;AACxB,EAAA,IAAI;AACF,IAAA,GAAA,CAAI,KAAK,mCAAmC,CAAA;AAC5C,IAAA,MAAM,OAAA,GAAU,MAAM,MAAA,CAAO,GAAA,EAAK,cAAc,GAAG,CAAA;AACnD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAElC,IAAA,MAAM,GAAA,GAAM,MAAM,UAAA,CAAW,IAAA;AAAA,MAC3B,GAAG,WAAW,CAAA,QAAA,CAAA;AAAA,MACd;AAAA,KACF;AACA,IAAA,MAAM,MAAA,GAAS,wBAAA,CAAyB,KAAA,CAAM,GAAG,CAAA;AAEjD,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,MAAA,CAAO,MAAA,CAAO,SAAS,CAAA;AAAA,MAC3B,IAAA,EAAM,OAAO,WAAA,IAAe,SAAA;AAAA,MAC5B,GAAA,EAAK,CAAA,0CAAA,EAA6C,MAAA,CAAO,SAAS,CAAA;AAAA,KACpE;AAAA,EACF,SAAS,KAAA,EAAO;AACd,IAAA,MAAMD,qBAAAA,CAAsB,wBAAA,EAA0B,MAAA,EAAW,KAAK,CAAA;AAAA,EACxE;AACF,CAAA;AAEA,IAAM,YAAA,GAAe,OACnB,UAAA,EACA,GAAA,EACA,OAAA,KAC8B;AAC9B,EAAA,IAAI;AACF,IAAA,GAAA,CAAI,KAAK,sCAAsC,CAAA;AAC/C,IAAA,MAAM,KAAA,GAAQ,SAAS,MAAA,IAAU,CAAA;AACjC,IAAA,MAAM,KAAA,GAAQ,SAAS,KAAA,IAAS,EAAA;AAChC,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB;AAAA,MACjC,KAAA,EAAO,OAAO,KAAK,CAAA;AAAA,MACnB,KAAA,EAAO,OAAO,KAAK;AAAA,KACpB,CAAA;AAED,IAAA,MAAM,GAAA,GAAM,MAAM,UAAA,CAAW,GAAA;AAAA,MAC3B,CAAA,EAAG,WAAW,CAAA,UAAA,EAAa,MAAM,CAAA;AAAA,KACnC;AACA,IAAA,MAAM,QAAA,GAAW,0BAAA,CAA2B,KAAA,EAAM,CAAE,MAAM,GAAG,CAAA;AAC7D,IAAA,OAAO,QAAA,CAAS,IAAI,mBAAmB,CAAA;AAAA,EACzC,SAAS,KAAA,EAAO;AACd,IAAA,MAAMA,qBAAAA,CAAsB,yBAAA,EAA2B,MAAA,EAAW,KAAK,CAAA;AAAA,EACzE;AACF,CAAA;AAEO,IAAM,0BAAA,GAA6B,CACxC,UAAA,EACA,MAAA,KACwB;AACxB,EAAA,MAAM,GAAA,GAAM,UAAUE,mBAAAA,EAAoB;AAC1C,EAAA,MAAM,YAAA,GAAe,mBAAmB,GAAG,CAAA;AAE3C,EAAA,OAAO;AAAA,IACL,MAAM,CAAC,GAAA,KAAQ,YAAY,GAAA,EAAK,UAAA,EAAY,cAAc,GAAG,CAAA;AAAA,IAC7D,MAAM,CAAC,IAAA,KAAS,YAAA,CAAa,UAAA,EAAY,KAAK,IAAI;AAAA,GACpD;AACF,CAAA;;;AChFO,IAAM,yBAAA,GAA4B,CACvC,OAAA,KACG;AACH,EAAA,MAAM,EAAE,IAAA,EAAM,UAAA,EAAW,GAAI,yBAAyB,OAAO,CAAA;AAC7D,EAAA,MAAM,OAAA,GAAU,0BAAA,CAA2B,UAAA,EAAY,OAAA,EAAS,MAAM,CAAA;AACtE,EAAA,OAAO,EAAE,MAAM,OAAA,EAAQ;AACzB;ACPA,IAAM,YAAA,GAAe,IAAA,CAAK,OAAA,EAAQ,EAAG,WAAW,oBAAoB,CAAA;AAE7D,IAAM,oBAAA,GAAuB,CAClC,QAAA,GAAmB,YAAA,MACH;AAAA,EAChB,IAAA,EAAM,OAAO,MAAA,KAAsB;AACjC,IAAA,MAAM,MAAM,OAAA,CAAQ,QAAQ,GAAG,EAAE,SAAA,EAAW,MAAM,CAAA;AAClD,IAAA,MAAM,UAAU,QAAA,EAAU,IAAA,CAAK,UAAU,MAAA,EAAQ,IAAA,EAAM,CAAC,CAAA,EAAG;AAAA,MACzD,QAAA,EAAU,OAAA;AAAA,MACV,IAAA,EAAM;AAAA,KACP,CAAA;AAAA,EACH,CAAA;AAAA,EAEA,MAAM,YAAuC;AAC3C,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,QAAA,EAAU,OAAO,CAAA;AAChD,MAAA,OAAO,IAAA,CAAK,MAAM,OAAO,CAAA;AAAA,IAC3B,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF,CAAA;AAAA,EAEA,OAAO,YAAY;AACjB,IAAA,IAAI;AACF,MAAA,MAAM,OAAO,QAAQ,CAAA;AAAA,IACvB,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AACF,CAAA;;;AChCO,IAAM,yBAAyB,MAAkB;AACtD,EAAA,IAAI,MAAA,GAA2B,IAAA;AAE/B,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,OAAO,MAAA,KAAsB;AACjC,MAAA,MAAA,GAAS,MAAA;AAAA,IACX,CAAA;AAAA,IACA,MAAM,YAAY,MAAA;AAAA,IAClB,OAAO,YAAY;AACjB,MAAA,MAAA,GAAS,IAAA;AAAA,IACX;AAAA,GACF;AACF","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
|
+
}
|