@gooin/garmin-connect 1.6.9 → 1.6.10

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.
Files changed (62) hide show
  1. package/dist/common/HttpClient.d.ts +37 -37
  2. package/dist/common/HttpClient.js +345 -332
  3. package/dist/common/HttpClient.js.map +1 -1
  4. package/dist/garmin/GarminConnect.d.ts +84 -84
  5. package/dist/garmin/GarminConnect.js +470 -470
  6. package/dist/garmin/UrlClass.d.ts +43 -43
  7. package/dist/garmin/UrlClass.js +124 -124
  8. package/dist/garmin/common/DateUtils.d.ts +6 -6
  9. package/dist/garmin/common/DateUtils.js +37 -37
  10. package/dist/garmin/common/HydrationUtils.d.ts +2 -2
  11. package/dist/garmin/common/HydrationUtils.js +15 -15
  12. package/dist/garmin/common/WeightUtils.d.ts +1 -1
  13. package/dist/garmin/common/WeightUtils.js +8 -8
  14. package/dist/garmin/types/activity.d.ts +408 -408
  15. package/dist/garmin/types/activity.js +26 -26
  16. package/dist/garmin/types/course.d.ts +146 -146
  17. package/dist/garmin/types/course.js +2 -2
  18. package/dist/garmin/types/golf.d.ts +67 -67
  19. package/dist/garmin/types/golf.js +2 -2
  20. package/dist/garmin/types/heartrate.d.ts +22 -22
  21. package/dist/garmin/types/heartrate.js +1 -1
  22. package/dist/garmin/types/hydration.d.ts +28 -28
  23. package/dist/garmin/types/hydration.js +1 -1
  24. package/dist/garmin/types/index.d.ts +541 -541
  25. package/dist/garmin/types/index.js +16 -16
  26. package/dist/garmin/types/sleep.d.ts +120 -120
  27. package/dist/garmin/types/sleep.js +2 -2
  28. package/dist/garmin/types/weight.d.ts +42 -42
  29. package/dist/garmin/types/weight.js +2 -2
  30. package/dist/garmin/workouts/Running.d.ts +16 -16
  31. package/dist/garmin/workouts/Running.js +46 -46
  32. package/dist/garmin/workouts/templates/RunningTemplate.d.ts +68 -68
  33. package/dist/garmin/workouts/templates/RunningTemplate.js +77 -77
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +8 -8
  36. package/dist/utils.d.ts +3 -3
  37. package/dist/utils.js +36 -36
  38. package/package.json +1 -1
  39. package/dist/common/CFClient.d.ts +0 -22
  40. package/dist/common/CFClient.js +0 -137
  41. package/dist/common/CFClient.js.map +0 -1
  42. package/dist/common/DateUtils.d.ts +0 -1
  43. package/dist/common/DateUtils.js +0 -11
  44. package/dist/common/DateUtils.js.map +0 -1
  45. package/dist/garmin/GarminConnect copy.d.ts +0 -76
  46. package/dist/garmin/GarminConnect copy.js +0 -397
  47. package/dist/garmin/GarminConnect copy.js.map +0 -1
  48. package/dist/garmin/GarminConnect_new.d.ts +0 -76
  49. package/dist/garmin/GarminConnect_new.js +0 -397
  50. package/dist/garmin/GarminConnect_new.js.map +0 -1
  51. package/dist/garmin/GarminConnect_old.d.ts +0 -75
  52. package/dist/garmin/GarminConnect_old.js +0 -386
  53. package/dist/garmin/GarminConnect_old.js.map +0 -1
  54. package/dist/garmin/Urls copy.d.ts +0 -66
  55. package/dist/garmin/Urls copy.js +0 -109
  56. package/dist/garmin/Urls copy.js.map +0 -1
  57. package/dist/garmin/Urls.d.ts +0 -64
  58. package/dist/garmin/Urls.js +0 -104
  59. package/dist/garmin/Urls.js.map +0 -1
  60. package/dist/garmin/types.d.ts +0 -927
  61. package/dist/garmin/types.js +0 -17
  62. package/dist/garmin/types.js.map +0 -1
@@ -1,333 +1,346 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.HttpClient = void 0;
7
- const axios_1 = __importDefault(require("axios"));
8
- const form_data_1 = __importDefault(require("form-data"));
9
- const lodash_1 = __importDefault(require("lodash"));
10
- const luxon_1 = require("luxon");
11
- const oauth_1_0a_1 = __importDefault(require("oauth-1.0a"));
12
- const qs_1 = __importDefault(require("qs"));
13
- const node_crypto_1 = __importDefault(require("node:crypto"));
14
- const CSRF_RE = new RegExp('name="_csrf"\\s+value="(.+?)"');
15
- const TICKET_RE = new RegExp('ticket=([^"]+)"');
16
- const ACCOUNT_LOCKED_RE = new RegExp('var statuss*=s*"([^"]*)"');
17
- const PAGE_TITLE_RE = new RegExp('<title>([^<]*)</title>');
18
- const USER_AGENT_CONNECTMOBILE = 'com.garmin.android.apps.connectmobile';
19
- const 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';
20
- const OAUTH_CONSUMER_URL = 'https://thegarth.s3.amazonaws.com/oauth_consumer.json';
21
- // refresh token
22
- let isRefreshing = false;
23
- let refreshSubscribers = [];
24
- class HttpClient {
25
- constructor(url) {
26
- this.url = url;
27
- this.client = axios_1.default.create();
28
- this.client.interceptors.response.use((response) => response, async (error) => {
29
- var _a;
30
- const originalRequest = error.config;
31
- // console.log('originalRequest:', originalRequest)
32
- // Auto Refresh token
33
- if (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 &&
34
- !(originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest._retry)) {
35
- if (!this.oauth2Token) {
36
- return;
37
- }
38
- if (isRefreshing) {
39
- try {
40
- const token = await new Promise((resolve) => {
41
- refreshSubscribers.push((token) => {
42
- resolve(token);
43
- });
44
- });
45
- originalRequest.headers.Authorization = `Bearer ${token}`;
46
- return this.client(originalRequest);
47
- }
48
- catch (err) {
49
- console.log('err:', err);
50
- return Promise.reject(err);
51
- }
52
- }
53
- originalRequest._retry = true;
54
- isRefreshing = true;
55
- // console.log('interceptors: refreshOauth2Token start');
56
- await this.refreshOauth2Token();
57
- // console.log('interceptors: refreshOauth2Token end');
58
- isRefreshing = false;
59
- refreshSubscribers.forEach((subscriber) => subscriber(this.oauth2Token.access_token));
60
- refreshSubscribers = [];
61
- originalRequest.headers.Authorization = `Bearer ${this.oauth2Token.access_token}`;
62
- return this.client(originalRequest);
63
- }
64
- if (axios_1.default.isAxiosError(error)) {
65
- if (error === null || error === void 0 ? void 0 : error.response)
66
- this.handleError(error === null || error === void 0 ? void 0 : error.response);
67
- }
68
- throw error;
69
- });
70
- this.client.interceptors.request.use(async (config) => {
71
- if (this.oauth2Token) {
72
- config.headers.Authorization =
73
- 'Bearer ' + this.oauth2Token.access_token;
74
- }
75
- return config;
76
- });
77
- }
78
- async fetchOauthConsumer() {
79
- const response = await axios_1.default.get(OAUTH_CONSUMER_URL);
80
- this.OAUTH_CONSUMER = {
81
- key: response.data.consumer_key,
82
- secret: response.data.consumer_secret
83
- };
84
- }
85
- async checkTokenVaild() {
86
- if (this.oauth2Token) {
87
- if (this.oauth2Token.expires_at < luxon_1.DateTime.now().toSeconds()) {
88
- console.error('Token expired!');
89
- await this.refreshOauth2Token();
90
- }
91
- }
92
- }
93
- async get(url, config) {
94
- const response = await this.client.get(url, config);
95
- return response === null || response === void 0 ? void 0 : response.data;
96
- }
97
- async post(url, data, config) {
98
- const response = await this.client.post(url, data, config);
99
- return response === null || response === void 0 ? void 0 : response.data;
100
- }
101
- async put(url, data, config) {
102
- const response = await this.client.put(url, data, config);
103
- return response === null || response === void 0 ? void 0 : response.data;
104
- }
105
- async delete(url, config) {
106
- const response = await this.client.post(url, null, {
107
- ...config,
108
- headers: {
109
- ...config === null || config === void 0 ? void 0 : config.headers,
110
- 'X-Http-Method-Override': 'DELETE'
111
- }
112
- });
113
- return response === null || response === void 0 ? void 0 : response.data;
114
- }
115
- setCommonHeader(headers) {
116
- lodash_1.default.each(headers, (headerValue, key) => {
117
- this.client.defaults.headers.common[key] = headerValue;
118
- });
119
- }
120
- handleError(response) {
121
- this.handleHttpError(response);
122
- }
123
- handleHttpError(response) {
124
- const { status, statusText, data } = response;
125
- const msg = `ERROR: (${status}), ${statusText}, ${JSON.stringify(data)}`;
126
- console.error(msg);
127
- throw new Error(msg);
128
- }
129
- /**
130
- * Login to Garmin Connect
131
- * @param username
132
- * @param password
133
- * @returns {Promise<HttpClient>}
134
- */
135
- async login(username, password) {
136
- await this.fetchOauthConsumer();
137
- // Step1-3: Get ticket from page.
138
- const ticket = await this.getLoginTicket(username, password);
139
- // Step4: Oauth1
140
- const oauth1 = await this.getOauth1Token(ticket);
141
- // TODO: Handle MFA
142
- // Step 5: Oauth2
143
- await this.exchange(oauth1);
144
- return this;
145
- }
146
- async getLoginTicket(username, password) {
147
- // Step1: Set cookie
148
- const step1Params = {
149
- clientId: 'GarminConnect',
150
- locale: 'en',
151
- service: this.url.GC_MODERN
152
- };
153
- const step1Url = `${this.url.GARMIN_SSO_EMBED}?${qs_1.default.stringify(step1Params)}`;
154
- // console.log('login - step1Url:', step1Url);
155
- await this.client.get(step1Url);
156
- // Step2 Get _csrf
157
- const step2Params = {
158
- id: 'gauth-widget',
159
- embedWidget: true,
160
- locale: 'en',
161
- gauthHost: this.url.GARMIN_SSO_EMBED
162
- };
163
- const step2Url = `${this.url.SIGNIN_URL}?${qs_1.default.stringify(step2Params)}`;
164
- // console.log('login - step2Url:', step2Url);
165
- const step2Result = await this.get(step2Url);
166
- // console.log('login - step2Result:', step2Result)
167
- const csrfRegResult = CSRF_RE.exec(step2Result);
168
- if (!csrfRegResult) {
169
- throw new Error('login - csrf not found');
170
- }
171
- const csrf_token = csrfRegResult[1];
172
- // console.log('login - csrf:', csrf_token);
173
- // Step3 Get ticket
174
- const signinParams = {
175
- id: 'gauth-widget',
176
- embedWidget: true,
177
- clientId: 'GarminConnect',
178
- locale: 'en',
179
- gauthHost: this.url.GARMIN_SSO_EMBED,
180
- service: this.url.GARMIN_SSO_EMBED,
181
- source: this.url.GARMIN_SSO_EMBED,
182
- redirectAfterAccountLoginUrl: this.url.GARMIN_SSO_EMBED,
183
- redirectAfterAccountCreationUrl: this.url.GARMIN_SSO_EMBED
184
- };
185
- const step3Url = `${this.url.SIGNIN_URL}?${qs_1.default.stringify(signinParams)}`;
186
- // console.log('login - step3Url:', step3Url);
187
- const step3Form = new form_data_1.default();
188
- step3Form.append('username', username);
189
- step3Form.append('password', password);
190
- step3Form.append('embed', 'true');
191
- step3Form.append('_csrf', csrf_token);
192
- const step3Result = await this.post(step3Url, step3Form, {
193
- headers: {
194
- 'Content-Type': 'application/x-www-form-urlencoded',
195
- Dnt: 1,
196
- Origin: this.url.GARMIN_SSO_ORIGIN,
197
- Referer: this.url.SIGNIN_URL,
198
- 'User-Agent': USER_AGENT_BROWSER
199
- }
200
- });
201
- // console.log('step3Result:', step3Result)
202
- this.handleAccountLocked(step3Result);
203
- this.handlePageTitle(step3Result);
204
- this.handleMFA(step3Result);
205
- const ticketRegResult = TICKET_RE.exec(step3Result);
206
- if (!ticketRegResult) {
207
- throw new Error('login failed (Ticket not found or MFA), please check username and password');
208
- }
209
- const ticket = ticketRegResult[1];
210
- return ticket;
211
- }
212
- // TODO: Handle MFA
213
- handleMFA(htmlStr) { }
214
- // TODO: Handle Phone number
215
- handlePageTitle(htmlStr) {
216
- const pageTitileRegResult = PAGE_TITLE_RE.exec(htmlStr);
217
- if (pageTitileRegResult) {
218
- const title = pageTitileRegResult[1];
219
- console.log('login page title:', title);
220
- if (lodash_1.default.includes(title, 'Update Phone Number')) {
221
- // current I don't know where to update it
222
- // See: https://github.com/matin/garth/issues/19
223
- throw new Error('login failed (Update Phone number), please update your phone number, See: https://github.com/matin/garth/issues/19');
224
- }
225
- }
226
- }
227
- handleAccountLocked(htmlStr) {
228
- const accountLockedRegResult = ACCOUNT_LOCKED_RE.exec(htmlStr);
229
- if (accountLockedRegResult) {
230
- const msg = accountLockedRegResult[1];
231
- console.error(msg);
232
- throw new Error('login failed (AccountLocked), please open connect web page to unlock your account');
233
- }
234
- }
235
- async refreshOauth2Token() {
236
- if (!this.OAUTH_CONSUMER) {
237
- await this.fetchOauthConsumer();
238
- }
239
- if (!this.oauth2Token || !this.oauth1Token) {
240
- throw new Error('No Oauth2Token or Oauth1Token');
241
- }
242
- const oauth1 = {
243
- oauth: this.getOauthClient(this.OAUTH_CONSUMER),
244
- token: this.oauth1Token
245
- };
246
- await this.exchange(oauth1);
247
- console.log('Oauth2 token refreshed!');
248
- }
249
- async getOauth1Token(ticket) {
250
- if (!this.OAUTH_CONSUMER) {
251
- throw new Error('No OAUTH_CONSUMER');
252
- }
253
- const params = {
254
- ticket,
255
- 'login-url': this.url.GARMIN_SSO_EMBED,
256
- 'accepts-mfa-tokens': true
257
- };
258
- const url = `${this.url.OAUTH_URL}/preauthorized?${qs_1.default.stringify(params)}`;
259
- const oauth = this.getOauthClient(this.OAUTH_CONSUMER);
260
- const step4RequestData = {
261
- url: url,
262
- method: 'GET'
263
- };
264
- const headers = oauth.toHeader(oauth.authorize(step4RequestData));
265
- // console.log('getOauth1Token - headers:', headers);
266
- const response = await this.get(url, {
267
- headers: {
268
- ...headers,
269
- 'User-Agent': USER_AGENT_CONNECTMOBILE
270
- }
271
- });
272
- // console.log('getOauth1Token - response:', response);
273
- const token = qs_1.default.parse(response);
274
- // console.log('getOauth1Token - token:', token);
275
- this.oauth1Token = token;
276
- return { token, oauth };
277
- }
278
- getOauthClient(consumer) {
279
- const oauth = new oauth_1_0a_1.default({
280
- consumer: consumer,
281
- signature_method: 'HMAC-SHA1',
282
- hash_function(base_string, key) {
283
- return node_crypto_1.default
284
- .createHmac('sha1', key)
285
- .update(base_string)
286
- .digest('base64');
287
- }
288
- });
289
- return oauth;
290
- }
291
- //
292
- async exchange(oauth1) {
293
- const token = {
294
- key: oauth1.token.oauth_token,
295
- secret: oauth1.token.oauth_token_secret
296
- };
297
- // console.log('exchange - token:', token);
298
- const baseUrl = `${this.url.OAUTH_URL}/exchange/user/2.0`;
299
- const requestData = {
300
- url: baseUrl,
301
- method: 'POST',
302
- data: null
303
- };
304
- const step5AuthData = oauth1.oauth.authorize(requestData, token);
305
- // console.log('login - step5AuthData:', step5AuthData);
306
- const url = `${baseUrl}?${qs_1.default.stringify(step5AuthData)}`;
307
- // console.log('exchange - url:', url);
308
- this.oauth2Token = undefined;
309
- const response = await this.post(url, null, {
310
- headers: {
311
- 'User-Agent': USER_AGENT_CONNECTMOBILE,
312
- 'Content-Type': 'application/x-www-form-urlencoded'
313
- }
314
- });
315
- // console.log('exchange - response:', response);
316
- this.oauth2Token = this.setOauth2TokenExpiresAt(response);
317
- // console.log('exchange - oauth2Token:', this.oauth2Token);
318
- }
319
- setOauth2TokenExpiresAt(token) {
320
- // human readable date
321
- token['last_update_date'] = luxon_1.DateTime.now().toLocal().toString();
322
- token['expires_date'] = luxon_1.DateTime.fromSeconds(luxon_1.DateTime.now().toSeconds() + token['expires_in'])
323
- .toLocal()
324
- .toString();
325
- // timestamp for check expired
326
- token['expires_at'] = luxon_1.DateTime.now().toSeconds() + token['expires_in'];
327
- token['refresh_token_expires_at'] =
328
- luxon_1.DateTime.now().toSeconds() + token['refresh_token_expires_in'];
329
- return token;
330
- }
331
- }
332
- exports.HttpClient = HttpClient;
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpClient = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const form_data_1 = __importDefault(require("form-data"));
9
+ const lodash_1 = __importDefault(require("lodash"));
10
+ const luxon_1 = require("luxon");
11
+ const oauth_1_0a_1 = __importDefault(require("oauth-1.0a"));
12
+ const qs_1 = __importDefault(require("qs"));
13
+ const node_crypto_1 = __importDefault(require("node:crypto"));
14
+ const CSRF_RE = new RegExp('name="_csrf"\\s+value="(.+?)"');
15
+ const TICKET_RE = new RegExp('ticket=([^"]+)"');
16
+ const ACCOUNT_LOCKED_RE = new RegExp('var statuss*=s*"([^"]*)"');
17
+ const PAGE_TITLE_RE = new RegExp('<title>([^<]*)</title>');
18
+ const USER_AGENT_CONNECTMOBILE = 'com.garmin.android.apps.connectmobile';
19
+ const 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';
20
+ const OAUTH_CONSUMER_URL = 'https://thegarth.s3.amazonaws.com/oauth_consumer.json';
21
+ const HTTP_STATUS = {
22
+ UNAUTHORIZED: 401
23
+ };
24
+ let tokenRefreshPromise = null;
25
+ let refreshSubscribers = [];
26
+ class HttpClient {
27
+ constructor(url) {
28
+ this.url = url;
29
+ this.client = axios_1.default.create({
30
+ timeout: 5000,
31
+ timeoutErrorMessage: 'Request Timeout: >5s'
32
+ });
33
+ this.client.interceptors.response.use((response) => response, async (error) => {
34
+ var _a;
35
+ if (axios_1.default.isAxiosError(error) &&
36
+ error.code === 'ECONNABORTED') {
37
+ throw new Error(error.message || 'Request Timeout');
38
+ }
39
+ const originalRequest = error.config;
40
+ if (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === HTTP_STATUS.UNAUTHORIZED &&
41
+ !(originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest._retry)) {
42
+ if (!this.oauth2Token) {
43
+ throw new Error('No OAuth2 token available');
44
+ }
45
+ originalRequest._retry = true;
46
+ try {
47
+ if (!tokenRefreshPromise) {
48
+ tokenRefreshPromise =
49
+ this.refreshOauth2Token().finally(() => {
50
+ tokenRefreshPromise = null;
51
+ });
52
+ }
53
+ await tokenRefreshPromise;
54
+ originalRequest.headers.Authorization = `Bearer ${this.oauth2Token.access_token}`;
55
+ return this.client(originalRequest);
56
+ }
57
+ catch (err) {
58
+ console.error('Token refresh failed:', err);
59
+ throw err;
60
+ }
61
+ }
62
+ if (axios_1.default.isAxiosError(error) && error.response) {
63
+ this.handleError(error.response);
64
+ }
65
+ else {
66
+ // 处理没有response的情况
67
+ throw new Error('Network error or unknown error occurred');
68
+ }
69
+ throw error;
70
+ });
71
+ this.client.interceptors.request.use(async (config) => {
72
+ if (this.oauth2Token) {
73
+ config.headers.Authorization =
74
+ 'Bearer ' + this.oauth2Token.access_token;
75
+ }
76
+ return config;
77
+ });
78
+ }
79
+ async fetchOauthConsumer() {
80
+ const response = await axios_1.default.get(OAUTH_CONSUMER_URL);
81
+ this.OAUTH_CONSUMER = {
82
+ key: response.data.consumer_key,
83
+ secret: response.data.consumer_secret
84
+ };
85
+ }
86
+ async checkTokenVaild() {
87
+ if (this.oauth2Token) {
88
+ if (this.oauth2Token.expires_at < luxon_1.DateTime.now().toSeconds()) {
89
+ console.error('Token expired!');
90
+ await this.refreshOauth2Token();
91
+ }
92
+ }
93
+ }
94
+ async get(url, config) {
95
+ const response = await this.client.get(url, config);
96
+ return response === null || response === void 0 ? void 0 : response.data;
97
+ }
98
+ async post(url, data, config) {
99
+ const response = await this.client.post(url, data, config);
100
+ return response === null || response === void 0 ? void 0 : response.data;
101
+ }
102
+ async put(url, data, config) {
103
+ const response = await this.client.put(url, data, config);
104
+ return response === null || response === void 0 ? void 0 : response.data;
105
+ }
106
+ async delete(url, config) {
107
+ const response = await this.client.post(url, null, {
108
+ ...config,
109
+ headers: {
110
+ ...config === null || config === void 0 ? void 0 : config.headers,
111
+ 'X-Http-Method-Override': 'DELETE'
112
+ }
113
+ });
114
+ return response === null || response === void 0 ? void 0 : response.data;
115
+ }
116
+ setCommonHeader(headers) {
117
+ lodash_1.default.each(headers, (headerValue, key) => {
118
+ this.client.defaults.headers.common[key] = headerValue;
119
+ });
120
+ }
121
+ handleError(response) {
122
+ this.handleHttpError(response);
123
+ }
124
+ handleHttpError(response) {
125
+ const { status, statusText, data } = response;
126
+ const errorMessage = {
127
+ status,
128
+ statusText,
129
+ data: typeof data === 'object' ? JSON.stringify(data) : data
130
+ };
131
+ console.error('HTTP Error:', errorMessage);
132
+ throw new Error(`HTTP Error (${status}): ${statusText}`);
133
+ }
134
+ /**
135
+ * Login to Garmin Connect
136
+ * @param username
137
+ * @param password
138
+ * @returns {Promise<HttpClient>}
139
+ */
140
+ async login(username, password) {
141
+ await this.fetchOauthConsumer();
142
+ // Step1-3: Get ticket from page.
143
+ const ticket = await this.getLoginTicket(username, password);
144
+ // Step4: Oauth1
145
+ const oauth1 = await this.getOauth1Token(ticket);
146
+ // TODO: Handle MFA
147
+ // Step 5: Oauth2
148
+ await this.exchange(oauth1);
149
+ return this;
150
+ }
151
+ async getLoginTicket(username, password) {
152
+ // Step1: Set cookie
153
+ const step1Params = {
154
+ clientId: 'GarminConnect',
155
+ locale: 'en',
156
+ service: this.url.GC_MODERN
157
+ };
158
+ const step1Url = `${this.url.GARMIN_SSO_EMBED}?${qs_1.default.stringify(step1Params)}`;
159
+ // console.log('login - step1Url:', step1Url);
160
+ await this.client.get(step1Url);
161
+ // Step2 Get _csrf
162
+ const step2Params = {
163
+ id: 'gauth-widget',
164
+ embedWidget: true,
165
+ locale: 'en',
166
+ gauthHost: this.url.GARMIN_SSO_EMBED
167
+ };
168
+ const step2Url = `${this.url.SIGNIN_URL}?${qs_1.default.stringify(step2Params)}`;
169
+ // console.log('login - step2Url:', step2Url);
170
+ const step2Result = await this.get(step2Url);
171
+ // console.log('login - step2Result:', step2Result)
172
+ const csrfRegResult = CSRF_RE.exec(step2Result);
173
+ if (!csrfRegResult) {
174
+ throw new Error('login - csrf not found');
175
+ }
176
+ const csrf_token = csrfRegResult[1];
177
+ // console.log('login - csrf:', csrf_token);
178
+ // Step3 Get ticket
179
+ const signinParams = {
180
+ id: 'gauth-widget',
181
+ embedWidget: true,
182
+ clientId: 'GarminConnect',
183
+ locale: 'en',
184
+ gauthHost: this.url.GARMIN_SSO_EMBED,
185
+ service: this.url.GARMIN_SSO_EMBED,
186
+ source: this.url.GARMIN_SSO_EMBED,
187
+ redirectAfterAccountLoginUrl: this.url.GARMIN_SSO_EMBED,
188
+ redirectAfterAccountCreationUrl: this.url.GARMIN_SSO_EMBED
189
+ };
190
+ const step3Url = `${this.url.SIGNIN_URL}?${qs_1.default.stringify(signinParams)}`;
191
+ // console.log('login - step3Url:', step3Url);
192
+ const step3Form = new form_data_1.default();
193
+ step3Form.append('username', username);
194
+ step3Form.append('password', password);
195
+ step3Form.append('embed', 'true');
196
+ step3Form.append('_csrf', csrf_token);
197
+ const step3Result = await this.post(step3Url, step3Form, {
198
+ headers: {
199
+ 'Content-Type': 'application/x-www-form-urlencoded',
200
+ Dnt: 1,
201
+ Origin: this.url.GARMIN_SSO_ORIGIN,
202
+ Referer: this.url.SIGNIN_URL,
203
+ 'User-Agent': USER_AGENT_BROWSER
204
+ }
205
+ });
206
+ // console.log('step3Result:', step3Result)
207
+ this.handleAccountLocked(step3Result);
208
+ this.handlePageTitle(step3Result);
209
+ this.handleMFA(step3Result);
210
+ const ticketRegResult = TICKET_RE.exec(step3Result);
211
+ if (!ticketRegResult) {
212
+ throw new Error('login failed (Ticket not found or MFA), please check username and password');
213
+ }
214
+ const ticket = ticketRegResult[1];
215
+ return ticket;
216
+ }
217
+ // TODO: Handle MFA
218
+ handleMFA(htmlStr) { }
219
+ // TODO: Handle Phone number
220
+ handlePageTitle(htmlStr) {
221
+ const pageTitileRegResult = PAGE_TITLE_RE.exec(htmlStr);
222
+ if (pageTitileRegResult) {
223
+ const title = pageTitileRegResult[1];
224
+ console.log('login page title:', title);
225
+ if (lodash_1.default.includes(title, 'Update Phone Number')) {
226
+ // current I don't know where to update it
227
+ // See: https://github.com/matin/garth/issues/19
228
+ throw new Error('login failed (Update Phone number), please update your phone number, See: https://github.com/matin/garth/issues/19');
229
+ }
230
+ }
231
+ }
232
+ handleAccountLocked(htmlStr) {
233
+ const accountLockedRegResult = ACCOUNT_LOCKED_RE.exec(htmlStr);
234
+ if (accountLockedRegResult) {
235
+ const msg = accountLockedRegResult[1];
236
+ console.error(msg);
237
+ throw new Error('login failed (AccountLocked), please open connect web page to unlock your account');
238
+ }
239
+ }
240
+ async refreshOauth2Token() {
241
+ try {
242
+ if (!this.OAUTH_CONSUMER) {
243
+ await this.fetchOauthConsumer();
244
+ }
245
+ if (!this.oauth2Token || !this.oauth1Token) {
246
+ throw new Error('Missing required tokens for refresh');
247
+ }
248
+ const oauth1 = {
249
+ oauth: this.getOauthClient(this.OAUTH_CONSUMER),
250
+ token: this.oauth1Token
251
+ };
252
+ await this.exchange(oauth1);
253
+ console.log('OAuth2 token refreshed successfully');
254
+ }
255
+ catch (error) {
256
+ console.error('Failed to refresh OAuth2 token:', error);
257
+ throw error;
258
+ }
259
+ }
260
+ async getOauth1Token(ticket) {
261
+ if (!this.OAUTH_CONSUMER) {
262
+ throw new Error('No OAUTH_CONSUMER');
263
+ }
264
+ const params = {
265
+ ticket,
266
+ 'login-url': this.url.GARMIN_SSO_EMBED,
267
+ 'accepts-mfa-tokens': true
268
+ };
269
+ const url = `${this.url.OAUTH_URL}/preauthorized?${qs_1.default.stringify(params)}`;
270
+ const oauth = this.getOauthClient(this.OAUTH_CONSUMER);
271
+ const step4RequestData = {
272
+ url: url,
273
+ method: 'GET'
274
+ };
275
+ const headers = oauth.toHeader(oauth.authorize(step4RequestData));
276
+ // console.log('getOauth1Token - headers:', headers);
277
+ const response = await this.get(url, {
278
+ headers: {
279
+ ...headers,
280
+ 'User-Agent': USER_AGENT_CONNECTMOBILE
281
+ }
282
+ });
283
+ // console.log('getOauth1Token - response:', response);
284
+ const token = qs_1.default.parse(response);
285
+ // console.log('getOauth1Token - token:', token);
286
+ this.oauth1Token = token;
287
+ return { token, oauth };
288
+ }
289
+ getOauthClient(consumer) {
290
+ const oauth = new oauth_1_0a_1.default({
291
+ consumer: consumer,
292
+ signature_method: 'HMAC-SHA1',
293
+ hash_function(base_string, key) {
294
+ return node_crypto_1.default
295
+ .createHmac('sha1', key)
296
+ .update(base_string)
297
+ .digest('base64');
298
+ }
299
+ });
300
+ return oauth;
301
+ }
302
+ //
303
+ async exchange(oauth1) {
304
+ const token = {
305
+ key: oauth1.token.oauth_token,
306
+ secret: oauth1.token.oauth_token_secret
307
+ };
308
+ // console.log('exchange - token:', token);
309
+ const baseUrl = `${this.url.OAUTH_URL}/exchange/user/2.0`;
310
+ const requestData = {
311
+ url: baseUrl,
312
+ method: 'POST',
313
+ data: null
314
+ };
315
+ const step5AuthData = oauth1.oauth.authorize(requestData, token);
316
+ // console.log('login - step5AuthData:', step5AuthData);
317
+ const url = `${baseUrl}?${qs_1.default.stringify(step5AuthData)}`;
318
+ // console.log('exchange - url:', url);
319
+ this.oauth2Token = undefined;
320
+ const response = await this.post(url, null, {
321
+ headers: {
322
+ 'User-Agent': USER_AGENT_CONNECTMOBILE,
323
+ 'Content-Type': 'application/x-www-form-urlencoded'
324
+ }
325
+ });
326
+ // console.log('exchange - response:', response);
327
+ this.oauth2Token = this.setOauth2TokenExpiresAt(response);
328
+ // console.log('exchange - oauth2Token:', this.oauth2Token);
329
+ }
330
+ setOauth2TokenExpiresAt(token) {
331
+ const now = luxon_1.DateTime.now();
332
+ const expiresAt = now.plus({ seconds: token.expires_in });
333
+ const refreshTokenExpiresAt = now.plus({
334
+ seconds: token.refresh_token_expires_in
335
+ });
336
+ return {
337
+ ...token,
338
+ last_update_date: now.toLocal().toString(),
339
+ expires_date: expiresAt.toLocal().toString(),
340
+ expires_at: expiresAt.toSeconds(),
341
+ refresh_token_expires_at: refreshTokenExpiresAt.toSeconds()
342
+ };
343
+ }
344
+ }
345
+ exports.HttpClient = HttpClient;
333
346
  //# sourceMappingURL=HttpClient.js.map