@gooin/garmin-connect 1.4.4 → 1.6.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.
Files changed (38) hide show
  1. package/README.md +225 -35
  2. package/dist/common/CFClient.d.ts +22 -0
  3. package/dist/common/CFClient.js +137 -140
  4. package/dist/common/CFClient.js.map +1 -0
  5. package/dist/common/DateUtils.d.ts +1 -0
  6. package/dist/common/DateUtils.js +11 -10
  7. package/dist/common/DateUtils.js.map +1 -0
  8. package/dist/common/HttpClient.d.ts +34 -0
  9. package/dist/common/HttpClient.js +299 -0
  10. package/dist/common/HttpClient.js.map +1 -0
  11. package/dist/garmin/GarminConnect.d.ts +40 -0
  12. package/dist/garmin/GarminConnect.js +178 -438
  13. package/dist/garmin/GarminConnect.js.map +1 -0
  14. package/dist/garmin/UrlClass.d.ts +25 -0
  15. package/dist/garmin/UrlClass.js +64 -0
  16. package/dist/garmin/UrlClass.js.map +1 -0
  17. package/dist/garmin/Urls.d.ts +64 -0
  18. package/dist/garmin/Urls.js +104 -102
  19. package/dist/garmin/Urls.js.map +1 -0
  20. package/dist/garmin/types.d.ts +706 -0
  21. package/dist/garmin/types.js +17 -0
  22. package/dist/garmin/types.js.map +1 -0
  23. package/dist/garmin/workouts/Running.d.ts +16 -0
  24. package/dist/garmin/workouts/Running.js +47 -53
  25. package/dist/garmin/workouts/Running.js.map +1 -0
  26. package/dist/garmin/workouts/templates/RunningTemplate.d.ts +68 -0
  27. package/dist/garmin/workouts/templates/RunningTemplate.js +78 -48
  28. package/dist/garmin/workouts/templates/RunningTemplate.js.map +1 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +9 -3
  31. package/dist/index.js.map +1 -0
  32. package/dist/utils.d.ts +3 -0
  33. package/dist/utils.js +46 -0
  34. package/dist/utils.js.map +1 -0
  35. package/examples/example.js +21 -6
  36. package/package.json +66 -51
  37. package/dist/common/Client.js +0 -189
  38. package/dist/garmin/workouts/index.js +0 -5
@@ -1,140 +1,137 @@
1
- const cloudscraper = require('cloudscraper');
2
- const qs = require('qs');
3
- const request = require('request');
4
- const fs = require('fs');
5
- const path = require('path');
6
-
7
- const asJson = (body) => {
8
- try {
9
- const jsonBody = JSON.parse(body);
10
- return jsonBody;
11
- } catch (e) {
12
- // Do nothing
13
- }
14
- return body;
15
- };
16
-
17
- class CFClient {
18
- constructor(headers) {
19
- this.cloudscraper = cloudscraper;
20
- this.queryString = qs;
21
- this.cookies = request.jar();
22
- this.headers = headers || {};
23
- }
24
-
25
- serializeCookies() {
26
- // eslint-disable-next-line no-underscore-dangle
27
- return this.cookies._jar.serializeSync();
28
- }
29
-
30
- importCookies(cookies) {
31
- // eslint-disable-next-line no-underscore-dangle
32
- const deserialized = this.cookies._jar.constructor.deserializeSync(cookies);
33
- this.cookies = request.jar();
34
- // eslint-disable-next-line no-underscore-dangle
35
- this.cookies._jar = deserialized;
36
- }
37
-
38
- async scraper(options) {
39
- return new Promise((resolve) => {
40
- this.cloudscraper(
41
- options,
42
- (err, res) => {
43
- resolve(res);
44
- },
45
- );
46
- });
47
- }
48
-
49
- /**
50
- * @param {string} downloadDir
51
- * @param {string} url
52
- * @param {*} data
53
- */
54
- async downloadBlob(downloadDir = '', url, data) {
55
- const queryData = this.queryString.stringify(data);
56
- const queryDataString = queryData ? `?${queryData}` : '';
57
- const options = {
58
- method: 'GET',
59
- jar: this.cookies,
60
- uri: `${url}${queryDataString}`,
61
- headers: this.headers,
62
- encoding: 0,
63
- };
64
- return new Promise((resolve) => {
65
- this.cloudscraper(
66
- options,
67
- async (err, response, body) => {
68
- const { headers } = response || {};
69
- const { 'content-disposition': contentDisposition } = headers || {};
70
- const downloadDirNormalized = path.normalize(downloadDir);
71
- if (contentDisposition) {
72
- const defaultName = `garmin_connect_download_${Date.now()}`;
73
- const [, fileName = defaultName] = contentDisposition.match(/filename="?([^"]+)"?/);
74
- const filePath = path.resolve(downloadDirNormalized, fileName);
75
- fs.writeFileSync(filePath, body);
76
- resolve(filePath);
77
- }
78
- },
79
- );
80
- });
81
- }
82
-
83
- async get(url, data) {
84
- const queryData = this.queryString.stringify(data);
85
- const queryDataString = queryData ? `?${queryData}` : '';
86
- const options = {
87
- method: 'GET',
88
- jar: this.cookies,
89
- uri: `${url}${queryDataString}`,
90
- headers: this.headers,
91
- };
92
- const { body } = await this.scraper(options);
93
- return asJson(body);
94
- }
95
-
96
- async post(url, data) {
97
- const options = {
98
- method: 'POST',
99
- uri: url,
100
- jar: this.cookies,
101
- formData: data,
102
- headers: this.headers,
103
- };
104
- const { body } = await this.scraper(options);
105
- return asJson(body);
106
- }
107
-
108
- async postJson(url, data, params, headers) {
109
- const options = {
110
- method: 'POST',
111
- uri: url,
112
- jar: this.cookies,
113
- json: data,
114
- headers: {
115
- ...this.headers,
116
- ...headers,
117
- 'Content-Type': 'application/json',
118
- },
119
- };
120
- const { body } = await this.scraper(options);
121
- return asJson(body);
122
- }
123
-
124
- async putJson(url, data) {
125
- const options = {
126
- method: 'PUT',
127
- uri: url,
128
- jar: this.cookies,
129
- json: data,
130
- headers: {
131
- ...this.headers,
132
- 'Content-Type': 'application/json',
133
- },
134
- };
135
- const { body } = await this.scraper(options);
136
- return asJson(body);
137
- }
138
- }
139
-
140
- module.exports = CFClient;
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
+ const cloudscraper_1 = __importDefault(require("cloudscraper"));
7
+ const request_1 = __importDefault(require("request"));
8
+ const tough_cookie_1 = require("tough-cookie");
9
+ const qs_1 = __importDefault(require("qs"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const asJson = (body) => {
13
+ try {
14
+ const jsonBody = JSON.parse(body);
15
+ return jsonBody;
16
+ }
17
+ catch (e) {
18
+ // Do nothing
19
+ }
20
+ return body;
21
+ };
22
+ class CFClient {
23
+ constructor(headers) {
24
+ this.cookies = request_1.default.jar();
25
+ this.headers = headers || {};
26
+ }
27
+ serializeCookies() {
28
+ var _a;
29
+ return (_a = this.cookies._jar) === null || _a === void 0 ? void 0 : _a.serializeSync();
30
+ }
31
+ importCookies(cookies) {
32
+ const deserialized = tough_cookie_1.CookieJar.deserializeSync(cookies);
33
+ this.cookies = request_1.default.jar();
34
+ this.cookies._jar = deserialized;
35
+ }
36
+ async scraper(options) {
37
+ return new Promise((resolve) => {
38
+ (0, cloudscraper_1.default)(options, (err, res) => {
39
+ resolve(res);
40
+ });
41
+ });
42
+ }
43
+ /**
44
+ * @param {string} downloadDir
45
+ * @param {string} url
46
+ * @param {*} data
47
+ */
48
+ async downloadBlob(downloadDir = '', url, data) {
49
+ const queryData = qs_1.default.stringify(data);
50
+ const queryDataString = queryData ? `?${queryData}` : '';
51
+ const options = {
52
+ method: 'GET',
53
+ jar: this.cookies,
54
+ uri: `${url}${queryDataString}`,
55
+ headers: this.headers,
56
+ encoding: null
57
+ };
58
+ return new Promise((resolve) => {
59
+ (0, cloudscraper_1.default)(options, async (err, response, body) => {
60
+ const { headers } = response || {};
61
+ const { 'content-disposition': contentDisposition } = headers || {};
62
+ const downloadDirNormalized = path_1.default.normalize(downloadDir);
63
+ if (contentDisposition) {
64
+ const defaultName = `garmin_connect_download_${Date.now()}`;
65
+ const [, fileName = defaultName] = contentDisposition.match(/filename="?([^"]+)"?/) || [];
66
+ const filePath = path_1.default.resolve(downloadDirNormalized, fileName);
67
+ fs_1.default.writeFileSync(filePath, body);
68
+ resolve(filePath);
69
+ }
70
+ });
71
+ });
72
+ }
73
+ async get(url, data) {
74
+ const queryData = qs_1.default.stringify(data);
75
+ const queryDataString = queryData ? `?${queryData}` : '';
76
+ const options = {
77
+ method: 'GET',
78
+ jar: this.cookies,
79
+ uri: `${url}${queryDataString}`,
80
+ headers: this.headers
81
+ };
82
+ const { body } = await this.scraper(options);
83
+ return asJson(body);
84
+ }
85
+ async post(url, data) {
86
+ const options = {
87
+ method: 'POST',
88
+ uri: url,
89
+ jar: this.cookies,
90
+ formData: data,
91
+ headers: this.headers
92
+ };
93
+ const { body } = await this.scraper(options);
94
+ return asJson(body);
95
+ }
96
+ async delete(url) {
97
+ const options = {
98
+ method: 'DELETE',
99
+ uri: url,
100
+ jar: this.cookies,
101
+ headers: this.headers
102
+ };
103
+ const { body } = await this.scraper(options);
104
+ return asJson(body);
105
+ }
106
+ async postJson(url, data, headers) {
107
+ const options = {
108
+ method: 'POST',
109
+ uri: url,
110
+ jar: this.cookies,
111
+ json: data,
112
+ headers: {
113
+ ...this.headers,
114
+ ...headers,
115
+ 'Content-Type': 'application/json'
116
+ }
117
+ };
118
+ const { body } = await this.scraper(options);
119
+ return asJson(body);
120
+ }
121
+ async putJson(url, data) {
122
+ const options = {
123
+ method: 'PUT',
124
+ uri: url,
125
+ jar: this.cookies,
126
+ json: data,
127
+ headers: {
128
+ ...this.headers,
129
+ 'Content-Type': 'application/json'
130
+ }
131
+ };
132
+ const { body } = await this.scraper(options);
133
+ return asJson(body);
134
+ }
135
+ }
136
+ exports.default = CFClient;
137
+ //# sourceMappingURL=CFClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CFClient.js","sourceRoot":"","sources":["../../src/common/CFClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function toDateString(date: Date): string;
@@ -1,10 +1,11 @@
1
- const toDateString = (date) => {
2
- const offset = date.getTimezoneOffset();
3
- const offsetDate = new Date(date.getTime() - (offset * 60 * 1000));
4
- const [dateString] = offsetDate.toISOString().split('T');
5
- return dateString;
6
- };
7
-
8
- module.exports = {
9
- toDateString,
10
- };
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toDateString = void 0;
4
+ function toDateString(date) {
5
+ const offset = date.getTimezoneOffset();
6
+ const offsetDate = new Date(date.getTime() - offset * 60 * 1000);
7
+ const [dateString] = offsetDate.toISOString().split('T');
8
+ return dateString;
9
+ }
10
+ exports.toDateString = toDateString;
11
+ //# sourceMappingURL=DateUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DateUtils.js","sourceRoot":"","sources":["../../src/common/DateUtils.ts"],"names":[],"mappings":";;;AAAA,SAAgB,YAAY,CAAC,IAAU;IACnC,MAAM,MAAM,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;IACxC,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACjE,MAAM,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzD,OAAO,UAAU,CAAC;AACtB,CAAC;AALD,oCAKC"}
@@ -0,0 +1,34 @@
1
+ import { AxiosInstance, AxiosRequestConfig, AxiosResponse, RawAxiosRequestHeaders } from 'axios';
2
+ import OAuth from 'oauth-1.0a';
3
+ import { UrlClass } from '../garmin/UrlClass';
4
+ import { IOauth1, IOauth1Consumer, IOauth1Token, IOauth2Token } from '../garmin/types';
5
+ export declare class HttpClient {
6
+ client: AxiosInstance;
7
+ url: UrlClass;
8
+ oauth1Token: IOauth1Token | undefined;
9
+ oauth2Token: IOauth2Token | undefined;
10
+ OAUTH_CONSUMER: IOauth1Consumer | undefined;
11
+ constructor(url: UrlClass);
12
+ fetchOauthConsumer(): Promise<void>;
13
+ checkTokenVaild(): Promise<void>;
14
+ get<T>(url: string, config?: AxiosRequestConfig<any>): Promise<T>;
15
+ post<T>(url: string, data: any, config?: AxiosRequestConfig<any>): Promise<T>;
16
+ setCommonHeader(headers: RawAxiosRequestHeaders): void;
17
+ handleError(response: AxiosResponse): void;
18
+ handleHttpError(response: AxiosResponse): void;
19
+ /**
20
+ * Login to Garmin Connect
21
+ * @param username
22
+ * @param password
23
+ * @returns {Promise<HttpClient>}
24
+ */
25
+ login(username: string, password: string): Promise<HttpClient>;
26
+ private getLoginTicket;
27
+ handleMFA(htmlStr: string): void;
28
+ handleAccountLocked(htmlStr: string): void;
29
+ refreshOauth2Token(): Promise<void>;
30
+ getOauth1Token(ticket: string): Promise<IOauth1>;
31
+ getOauthClient(consumer: IOauth1Consumer): OAuth;
32
+ exchange(oauth1: IOauth1): Promise<void>;
33
+ setOauth2TokenExpiresAt(token: IOauth2Token): IOauth2Token;
34
+ }
@@ -0,0 +1,299 @@
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 crypto = require('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 USER_AGENT_CONNECTMOBILE = 'com.garmin.android.apps.connectmobile';
18
+ const USER_AGENT_BROWSER = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1';
19
+ const OAUTH_CONSUMER_URL = 'https://thegarth.s3.amazonaws.com/oauth_consumer.json';
20
+ // refresh token
21
+ let isRefreshing = false;
22
+ let refreshSubscribers = [];
23
+ class HttpClient {
24
+ constructor(url) {
25
+ this.url = url;
26
+ this.client = axios_1.default.create();
27
+ this.client.interceptors.response.use((response) => response, async (error) => {
28
+ const originalRequest = error.config;
29
+ // console.log('originalRequest:', originalRequest)
30
+ // Auto Refresh token
31
+ if (error.response.status === 401 && !originalRequest._retry) {
32
+ if (!this.oauth2Token) {
33
+ return;
34
+ }
35
+ if (isRefreshing) {
36
+ try {
37
+ const token = await new Promise((resolve) => {
38
+ refreshSubscribers.push((token) => {
39
+ resolve(token);
40
+ });
41
+ });
42
+ originalRequest.headers.Authorization = `Bearer ${token}`;
43
+ return this.client(originalRequest);
44
+ }
45
+ catch (err) {
46
+ console.log('err:', err);
47
+ return Promise.reject(err);
48
+ }
49
+ }
50
+ originalRequest._retry = true;
51
+ isRefreshing = true;
52
+ console.log('interceptors: refreshOauth2Token start');
53
+ await this.refreshOauth2Token();
54
+ console.log('interceptors: refreshOauth2Token end');
55
+ isRefreshing = false;
56
+ refreshSubscribers.forEach((subscriber) => subscriber(this.oauth2Token.access_token));
57
+ refreshSubscribers = [];
58
+ originalRequest.headers.Authorization = `Bearer ${this.oauth2Token.access_token}`;
59
+ return this.client(originalRequest);
60
+ }
61
+ if (axios_1.default.isAxiosError(error)) {
62
+ if (error === null || error === void 0 ? void 0 : error.response)
63
+ this.handleError(error === null || error === void 0 ? void 0 : error.response);
64
+ }
65
+ throw error;
66
+ });
67
+ this.client.interceptors.request.use(async (config) => {
68
+ if (this.oauth2Token) {
69
+ config.headers.Authorization =
70
+ 'Bearer ' + this.oauth2Token.access_token;
71
+ }
72
+ return config;
73
+ });
74
+ }
75
+ async fetchOauthConsumer() {
76
+ const response = await axios_1.default.get(OAUTH_CONSUMER_URL);
77
+ this.OAUTH_CONSUMER = {
78
+ key: response.data.consumer_key,
79
+ secret: response.data.consumer_secret
80
+ };
81
+ }
82
+ async checkTokenVaild() {
83
+ if (this.oauth2Token) {
84
+ if (this.oauth2Token.expires_at < luxon_1.DateTime.now().toSeconds()) {
85
+ console.error('Token expired!');
86
+ await this.refreshOauth2Token();
87
+ }
88
+ }
89
+ }
90
+ async get(url, config) {
91
+ const response = await this.client.get(url, config);
92
+ return response === null || response === void 0 ? void 0 : response.data;
93
+ }
94
+ async post(url, data, config) {
95
+ const response = await this.client.post(url, data, config);
96
+ return response === null || response === void 0 ? void 0 : response.data;
97
+ }
98
+ setCommonHeader(headers) {
99
+ lodash_1.default.each(headers, (headerValue, key) => {
100
+ this.client.defaults.headers.common[key] = headerValue;
101
+ });
102
+ }
103
+ handleError(response) {
104
+ this.handleHttpError(response);
105
+ }
106
+ handleHttpError(response) {
107
+ const { status, statusText, data } = response;
108
+ const msg = `ERROR: (${status}), ${statusText}, ${JSON.stringify(data)}`;
109
+ console.error(msg);
110
+ throw new Error(msg);
111
+ }
112
+ /**
113
+ * Login to Garmin Connect
114
+ * @param username
115
+ * @param password
116
+ * @returns {Promise<HttpClient>}
117
+ */
118
+ async login(username, password) {
119
+ await this.fetchOauthConsumer();
120
+ // Step1-3: Get ticket from page.
121
+ const ticket = await this.getLoginTicket(username, password);
122
+ // Step4: Oauth1
123
+ const oauth1 = await this.getOauth1Token(ticket);
124
+ // TODO: Handle MFA
125
+ // Step 5: Oauth2
126
+ await this.exchange(oauth1);
127
+ return this;
128
+ }
129
+ async getLoginTicket(username, password) {
130
+ // Step1: Set cookie
131
+ const step1Params = {
132
+ clientId: 'GarminConnect',
133
+ locale: 'en',
134
+ service: this.url.GC_MODERN
135
+ };
136
+ const step1Url = `${this.url.GARMIN_SSO_EMBED}?${qs_1.default.stringify(step1Params)}`;
137
+ console.log('login - step1Url:', step1Url);
138
+ await this.client.get(step1Url);
139
+ // Step2 Get _csrf
140
+ const step2Params = {
141
+ id: 'gauth-widget',
142
+ embedWidget: true,
143
+ locale: 'en',
144
+ gauthHost: this.url.GARMIN_SSO_EMBED
145
+ };
146
+ const step2Url = `${this.url.SIGNIN_URL}?${qs_1.default.stringify(step2Params)}`;
147
+ console.log('login - step2Url:', step2Url);
148
+ const step2Result = await this.get(step2Url);
149
+ // console.log('login - step2Result:', step2Result)
150
+ const csrfRegResult = CSRF_RE.exec(step2Result);
151
+ if (!csrfRegResult) {
152
+ throw new Error('login - csrf not found');
153
+ }
154
+ const csrf_token = csrfRegResult[1];
155
+ console.log('login - csrf:', csrf_token);
156
+ // Step3 Get ticket
157
+ const signinParams = {
158
+ id: 'gauth-widget',
159
+ embedWidget: true,
160
+ clientId: 'GarminConnect',
161
+ locale: 'en',
162
+ gauthHost: this.url.GARMIN_SSO_EMBED,
163
+ service: this.url.GARMIN_SSO_EMBED,
164
+ source: this.url.GARMIN_SSO_EMBED,
165
+ redirectAfterAccountLoginUrl: this.url.GARMIN_SSO_EMBED,
166
+ redirectAfterAccountCreationUrl: this.url.GARMIN_SSO_EMBED
167
+ };
168
+ const step3Url = `${this.url.SIGNIN_URL}?${qs_1.default.stringify(signinParams)}`;
169
+ console.log('login - step3Url:', step3Url);
170
+ const step3Form = new form_data_1.default();
171
+ step3Form.append('username', username);
172
+ step3Form.append('password', password);
173
+ step3Form.append('embed', 'true');
174
+ step3Form.append('_csrf', csrf_token);
175
+ const step3Result = await this.post(step3Url, step3Form, {
176
+ headers: {
177
+ 'Content-Type': 'application/x-www-form-urlencoded',
178
+ Dnt: 1,
179
+ Origin: this.url.GARMIN_SSO_ORIGIN,
180
+ Referer: this.url.SIGNIN_URL,
181
+ 'User-Agent': USER_AGENT_BROWSER
182
+ }
183
+ });
184
+ // console.log('step3Result:', step3Result)
185
+ this.handleAccountLocked(step3Result);
186
+ this.handleMFA(step3Result);
187
+ const ticketRegResult = TICKET_RE.exec(step3Result);
188
+ if (!ticketRegResult) {
189
+ throw new Error('login failed (Ticket not found or MFA), please check username and password');
190
+ }
191
+ const ticket = ticketRegResult[1];
192
+ return ticket;
193
+ }
194
+ // TODO: Handle MFA
195
+ handleMFA(htmlStr) { }
196
+ handleAccountLocked(htmlStr) {
197
+ const accountLockedRegResult = ACCOUNT_LOCKED_RE.exec(htmlStr);
198
+ if (accountLockedRegResult) {
199
+ const msg = accountLockedRegResult[1];
200
+ console.error(msg);
201
+ throw new Error('login failed (AccountLocked), please open connect web page to unlock your account');
202
+ }
203
+ }
204
+ async refreshOauth2Token() {
205
+ if (!this.oauth2Token || !this.oauth1Token || !this.OAUTH_CONSUMER) {
206
+ throw new Error('No Oauth2Token or Oauth1Token or OAUTH_CONSUMER');
207
+ }
208
+ const oauth1 = {
209
+ oauth: this.getOauthClient(this.OAUTH_CONSUMER),
210
+ token: this.oauth1Token
211
+ };
212
+ await this.exchange(oauth1);
213
+ console.log('Oauth2 token refreshed!');
214
+ }
215
+ async getOauth1Token(ticket) {
216
+ if (!this.OAUTH_CONSUMER) {
217
+ throw new Error('No OAUTH_CONSUMER');
218
+ }
219
+ const params = {
220
+ ticket,
221
+ 'login-url': this.url.GARMIN_SSO_EMBED,
222
+ 'accepts-mfa-tokens': true
223
+ };
224
+ const url = `${this.url.OAUTH_URL}/preauthorized?${qs_1.default.stringify(params)}`;
225
+ const oauth = this.getOauthClient(this.OAUTH_CONSUMER);
226
+ const step4RequestData = {
227
+ url: url,
228
+ method: 'GET'
229
+ };
230
+ const headers = oauth.toHeader(oauth.authorize(step4RequestData));
231
+ console.log('getOauth1Token - headers:', headers);
232
+ const response = await this.get(url, {
233
+ headers: {
234
+ ...headers,
235
+ 'User-Agent': USER_AGENT_CONNECTMOBILE
236
+ }
237
+ });
238
+ console.log('getOauth1Token - response:', response);
239
+ const token = qs_1.default.parse(response);
240
+ console.log('getOauth1Token - token:', token);
241
+ this.oauth1Token = token;
242
+ return { token, oauth };
243
+ }
244
+ getOauthClient(consumer) {
245
+ const oauth = new oauth_1_0a_1.default({
246
+ consumer: consumer,
247
+ signature_method: 'HMAC-SHA1',
248
+ hash_function(base_string, key) {
249
+ return crypto
250
+ .createHmac('sha1', key)
251
+ .update(base_string)
252
+ .digest('base64');
253
+ }
254
+ });
255
+ return oauth;
256
+ }
257
+ //
258
+ async exchange(oauth1) {
259
+ const token = {
260
+ key: oauth1.token.oauth_token,
261
+ secret: oauth1.token.oauth_token_secret
262
+ };
263
+ console.log('exchange - token:', token);
264
+ const baseUrl = `${this.url.OAUTH_URL}/exchange/user/2.0`;
265
+ const requestData = {
266
+ url: baseUrl,
267
+ method: 'POST',
268
+ data: null
269
+ };
270
+ const step5AuthData = oauth1.oauth.authorize(requestData, token);
271
+ console.log('login - step5AuthData:', step5AuthData);
272
+ const url = `${baseUrl}?${qs_1.default.stringify(step5AuthData)}`;
273
+ console.log('exchange - url:', url);
274
+ this.oauth2Token = undefined;
275
+ const response = await this.post(url, null, {
276
+ headers: {
277
+ 'User-Agent': USER_AGENT_CONNECTMOBILE,
278
+ 'Content-Type': 'application/x-www-form-urlencoded'
279
+ }
280
+ });
281
+ console.log('exchange - response:', response);
282
+ this.oauth2Token = this.setOauth2TokenExpiresAt(response);
283
+ console.log('exchange - oauth2Token:', this.oauth2Token);
284
+ }
285
+ setOauth2TokenExpiresAt(token) {
286
+ // human readable date
287
+ token['last_update_date'] = luxon_1.DateTime.now().toLocal().toString();
288
+ token['expires_date'] = luxon_1.DateTime.fromSeconds(luxon_1.DateTime.now().toSeconds() + token['expires_in'])
289
+ .toLocal()
290
+ .toString();
291
+ // timestamp for check expired
292
+ token['expires_at'] = luxon_1.DateTime.now().toSeconds() + token['expires_in'];
293
+ token['refresh_token_expires_at'] =
294
+ luxon_1.DateTime.now().toSeconds() + token['refresh_token_expires_in'];
295
+ return token;
296
+ }
297
+ }
298
+ exports.HttpClient = HttpClient;
299
+ //# sourceMappingURL=HttpClient.js.map