diff --git a/README.md b/README.md index 8a0d010..afd2941 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,30 @@ # garmin-connect +## v1.6.0 refactor + +TODO: + +- [x] New HttpClient class +- [x] Login and get user token +- [x] Garmin URLs works with `garmin.cn` and `garmin.com` +- [x] Auto refresh Ouath2 token +- [x] Oauth1,Oauth2 token import and export. +- [x] Download Activity, countActivities, getActivities, getActivity, getUserProfile, getUserSettings +- [x] Upload Activity, delete Activity +- [ ] Implementation of other methods, such as Badge,Workout,Gear etc +- [ ] Handle MFA +- [x] Handle Account locked +- [ ] Unit test +- [ ] Listeners + +If something is not working, please check [https://connect.garmin.com/status/](https://connect.garmin.com/status/) first. + +Currently, most of previous features are working, but some of Rest API are not added, such as `Gear`,`Workout`,`Badge` etc. So if you need these features, please add a PR. + +All of above work inspired by [https://github.com/matin/garth](https://github.com/matin/garth). Many thanks. + +--- + A powerful JavaScript library for connecting to Garmin Connect for sending and receiving health and workout data. It comes with some predefined methods to get and set different kinds of data for your Garmin account, but also have the possibility to make [custom requests](#custom-requests) `GET`, `POST` and `PUT` are currently supported. This makes it easy to implement whatever may be missing to suite your needs. ## Prerequisites @@ -24,15 +49,54 @@ $ npm install garmin-connect ```js const { GarminConnect } = require('garmin-connect'); // Create a new Garmin Connect Client -const GCClient = new GarminConnect({"username": "my.email@example.com", "password": "MySecretPassword"}); +const GCClient = new GarminConnect({ + username: 'my.email@example.com', + password: 'MySecretPassword' +}); // Uses credentials from garmin.config.json or uses supplied params await GCClient.login(); -const userInfo = await GCClient.getUserInfo(); +const userProfile = await GCClient.getUserProfile(); ``` -Now you can check `userInfo.emailAddress` to verify that your login was successful. +Now you can check `userProfile.userName` to verify that your login was successful. -## Reusing your session +## Reusing your session(since v1.6.0) + +### Save token to file and reuse it. + +```js +GCClient.saveTokenToFile('/path/to/save/tokens'); +``` + +Result: + +```bash +$ ls /path/to/save/tokens +oauth1_token.json oauth2_token.json +``` + +Reuse token: + +```js +GCClient.loadTokenByFile('/path/to/save/tokens'); +``` + +### Or just save your token to db or other storage. + +```js +const oauth1 = GCClient.client.oauth1Token; +const oauth2 = GCClient.client.oauth2Token; +// save to db or other storage +... +``` + +Reuse token: + +```js +GCClient.loadToken(oauth1, oauth2); +``` + +## Reusing your session(depreated) This is an experimental feature and might not yet provide full stability. @@ -360,4 +424,3 @@ For now, this library only supports the following: - Get earned badges - Get available badges - Get details about one specific badge - diff --git a/examples/example.js b/examples/example.js index 6fc3c5f..97b94cf 100644 --- a/examples/example.js +++ b/examples/example.js @@ -1,18 +1,33 @@ -const { GarminConnect } = require('garmin-connect'); +const { GarminConnect } = require('../dist/index'); // Has to be run in an async function to be able to use the await keyword const main = async () => { // Create a new Garmin Connect Client - const GCClient = new GarminConnect(); + const GCClient = new GarminConnect({ + username: 'your-email', + password: 'your-password' + }); + + // TODO: Test China Domain + // China Domain + // const GCClient = new GarminConnect({ + // username: 'your-email', + // password: 'your-password' + // }, 'garmin.cn'); // Uses credentials from garmin.config.json or uses supplied params - await GCClient.login('my.email@example.com', 'MySecretPassword'); + await GCClient.login(); + + // // Get user info + // const info = await GCClient.getUserInfo(); - // Get user info - const info = await GCClient.getUserInfo(); + // Log info to make sure signin was successful + // console.log(info); + // // Get user settings + const settings = await GCClient.getUserSettings(); // Log info to make sure signin was successful - console.log(info); + console.log(settings); }; // Run the code diff --git a/package-lock.json b/package-lock.json index 519055e..0d73de1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,27 @@ { "name": "garmin-connect", - "version": "1.5.0", + "version": "1.6.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "garmin-connect", - "version": "1.5.0", + "version": "1.6.1", "license": "MIT", "dependencies": { "app-root-path": "^3.1.0", - "cloudscraper": "^4.6.0", - "qs": "^6.11.0", - "request": "^2.88.2" + "axios": "^1.5.1", + "crypto": "^1.0.1", + "form-data": "^4.0.0", + "lodash": "^4.17.21", + "luxon": "^3.4.3", + "oauth-1.0a": "^2.2.6", + "qs": "^6.11.2" }, "devDependencies": { "@types/app-root-path": "^1.2.4", + "@types/lodash": "^4.14.199", + "@types/luxon": "^3.3.2", "@types/node": "^18.11.15", "@types/qs": "^6.9.7", "@types/request-promise": "^4.1.48", @@ -43,6 +49,18 @@ "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.199", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", + "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", + "dev": true + }, + "node_modules/@types/luxon": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/@types/luxon/-/luxon-3.3.2.tgz", + "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -103,21 +121,6 @@ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "dev": true }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -168,59 +171,27 @@ "node": ">=8" } }, - "node_modules/asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "engines": { - "node": ">=0.8" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "engines": { - "node": "*" + "node_modules/axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, - "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -249,26 +220,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "node_modules/cloudscraper": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cloudscraper/-/cloudscraper-4.6.0.tgz", - "integrity": "sha512-42g6atOAQwhoMlzCYsB1238RYEQa3ibcxhjVeYuZQDLGSZjBNAKOlF/2kcPwZUhlRKA9LDwuYQ7/0LCoMui2ww==", - "dependencies": { - "request-promise": "^4.2.4" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "brotli": "^1.3.2", - "request": "^2.88.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -322,7 +273,8 @@ "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -338,16 +290,11 @@ "node": ">= 8" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -357,15 +304,6 @@ "node": ">=0.4.0" } }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -398,35 +336,36 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, "engines": { - "node": "*" + "node": ">= 6" } }, "node_modules/function-bind": { @@ -462,35 +401,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -522,20 +432,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -572,11 +468,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -589,50 +480,19 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/luxon": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/luxon/-/luxon-3.4.3.tgz", + "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "engines": { + "node": ">=12" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -716,13 +576,10 @@ "node": ">=8" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } + "node_modules/oauth-1.0a": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", + "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==" }, "node_modules/object-inspect": { "version": "1.11.0", @@ -774,11 +631,6 @@ "node": ">=8" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, "node_modules/pre-commit": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", @@ -978,17 +830,17 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, - "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -999,18 +851,10 @@ "once": "^1.3.1" } }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dependencies": { "side-channel": "^1.0.4" }, @@ -1042,114 +886,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", - "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", - "deprecated": "request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", - "dependencies": { - "bluebird": "^3.5.0", - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "request": "^2.34" - } - }, - "node_modules/request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "dependencies": { - "lodash": "^4.17.19" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "request": "^2.34" - } - }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1201,38 +937,6 @@ "os-shim": "^0.1.2" } }, - "node_modules/sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -1269,34 +973,6 @@ "node": ">=8" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -1316,42 +992,12 @@ "node": ">=4.2.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1393,6 +1039,18 @@ "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", "dev": true }, + "@types/lodash": { + "version": "4.14.199", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", + "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", + "dev": true + }, + "@types/luxon": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/@types/luxon/-/luxon-3.3.2.tgz", + "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==", + "dev": true + }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -1452,17 +1110,6 @@ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", "dev": true }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1495,33 +1142,20 @@ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "dev": true }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + "axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "balanced-match": { "version": "1.0.2", @@ -1529,19 +1163,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1567,19 +1188,6 @@ "get-intrinsic": "^1.0.2" } }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "cloudscraper": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cloudscraper/-/cloudscraper-4.6.0.tgz", - "integrity": "sha512-42g6atOAQwhoMlzCYsB1238RYEQa3ibcxhjVeYuZQDLGSZjBNAKOlF/2kcPwZUhlRKA9LDwuYQ7/0LCoMui2ww==", - "requires": { - "request-promise": "^4.2.4" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1624,7 +1232,8 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, "cross-spawn": { "version": "7.0.3", @@ -1637,28 +1246,16 @@ "which": "^2.0.1" } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1685,30 +1282,20 @@ "strip-final-newline": "^2.0.0" } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } }, "function-bind": { "version": "1.1.1", @@ -1734,28 +1321,6 @@ "pump": "^3.0.0" } }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -1775,16 +1340,6 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -1809,11 +1364,6 @@ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1826,47 +1376,16 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "luxon": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/luxon/-/luxon-3.4.3.tgz", + "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==" + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1929,10 +1448,10 @@ "path-key": "^3.0.0" } }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + "oauth-1.0a": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", + "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==" }, "object-inspect": { "version": "1.11.0", @@ -1969,11 +1488,6 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, "pre-commit": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", @@ -2125,17 +1639,17 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -2146,15 +1660,10 @@ "once": "^1.3.1" } }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "requires": { "side-channel": "^1.0.4" } @@ -2182,79 +1691,6 @@ } } }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" - } - } - }, - "request-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", - "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", - "requires": { - "bluebird": "^3.5.0", - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "requires": { - "lodash": "^4.17.19" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2296,27 +1732,6 @@ "os-shim": "^0.1.2" } }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -2349,28 +1764,6 @@ "has-flag": "^4.0.0" } }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -2383,35 +1776,12 @@ "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 85deb84..25dbd20 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,14 @@ { "name": "garmin-connect", - "version": "1.5.0", + "version": "1.6.5", "description": "Makes it simple to interface with Garmin Connect to get or set any data point", "main": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { "build": "tsc --build --clean && find ./dist -type d -empty -delete; tsc", + "build:windows": "tsc --build --clean && tsc", "build:watch": "npm run build -- --watch", + "build:watch-windows": "npm run build:windows -- --watch", "prettier:all": "prettier --write .", "pretty": "pretty-quick --staged", "prepack": "npm run build" @@ -35,6 +37,8 @@ ], "devDependencies": { "@types/app-root-path": "^1.2.4", + "@types/lodash": "^4.14.199", + "@types/luxon": "^3.3.2", "@types/node": "^18.11.15", "@types/qs": "^6.9.7", "@types/request-promise": "^4.1.48", @@ -50,9 +54,13 @@ "runkitExampleFilename": "./examples/example.js", "dependencies": { "app-root-path": "^3.1.0", - "cloudscraper": "^4.6.0", - "qs": "^6.11.0", - "request": "^2.88.2" + "axios": "^1.5.1", + "crypto": "^1.0.1", + "form-data": "^4.0.0", + "lodash": "^4.17.21", + "luxon": "^3.4.3", + "oauth-1.0a": "^2.2.6", + "qs": "^6.11.2" }, "pre-commit": "pretty" } diff --git a/src/common/CFClient.ts b/src/common/CFClient.ts deleted file mode 100644 index 2f36584..0000000 --- a/src/common/CFClient.ts +++ /dev/null @@ -1,147 +0,0 @@ -import cloudscraper, { Options, Response } from 'cloudscraper'; -import request, { Headers, CookieJar } from 'request'; -import { CookieJar as ToughCookieJar } from 'tough-cookie'; -import qs from 'qs'; -import fs from 'fs'; -import path from 'path'; - -const asJson = (body: string): T => { - try { - const jsonBody = JSON.parse(body); - return jsonBody as T; - } catch (e) { - // Do nothing - } - return body as T; -}; - -export default class CFClient { - private cookies: CookieJar & { _jar?: ToughCookieJar }; - private headers: Headers; - - constructor(headers: Headers) { - this.cookies = request.jar(); - this.headers = headers || {}; - } - - serializeCookies() { - return this.cookies._jar?.serializeSync(); - } - - importCookies(cookies: ToughCookieJar.Serialized) { - const deserialized = ToughCookieJar.deserializeSync(cookies); - this.cookies = request.jar(); - this.cookies._jar = deserialized; - } - - async scraper(options: Options): Promise { - return new Promise((resolve) => { - cloudscraper(options, (err, res) => { - resolve(res); - }); - }); - } - - /** - * @param {string} downloadDir - * @param {string} url - * @param {*} data - */ - async downloadBlob(downloadDir = '', url: string, data?: any) { - const queryData = qs.stringify(data); - const queryDataString = queryData ? `?${queryData}` : ''; - const options = { - method: 'GET', - jar: this.cookies, - uri: `${url}${queryDataString}`, - headers: this.headers, - encoding: null - } as Options; - return new Promise((resolve) => { - cloudscraper(options, async (err, response, body) => { - const { headers } = response || {}; - const { 'content-disposition': contentDisposition } = - headers || {}; - const downloadDirNormalized = path.normalize(downloadDir); - if (contentDisposition) { - const defaultName = `garmin_connect_download_${Date.now()}`; - const [, fileName = defaultName] = - contentDisposition.match(/filename="?([^"]+)"?/) || []; - const filePath = path.resolve( - downloadDirNormalized, - fileName - ); - fs.writeFileSync(filePath, body); - resolve(filePath); - } - }); - }); - } - - async get(url: string, data?: any) { - const queryData = qs.stringify(data); - const queryDataString = queryData ? `?${queryData}` : ''; - const options = { - method: 'GET', - jar: this.cookies, - uri: `${url}${queryDataString}`, - headers: this.headers - } as Options; - const { body } = await this.scraper(options); - return asJson(body); - } - - async post(url: string, data: any) { - const options = { - method: 'POST', - uri: url, - jar: this.cookies, - formData: data, - headers: this.headers - }; - const { body } = await this.scraper(options); - return asJson(body); - } - - async delete(url: string) { - const options = { - method: 'DELETE', - uri: url, - jar: this.cookies, - headers: this.headers - }; - const { body } = await this.scraper(options); - return asJson(body); - } - - async postJson(url: string, data: any, headers: Headers) { - const options = { - method: 'POST', - uri: url, - jar: this.cookies, - json: data, - headers: { - ...this.headers, - ...headers, - 'Content-Type': 'application/json' - } - }; - const { body } = await this.scraper(options); - return asJson(body); - } - - async putJson(url: string, data: any) { - const options = { - method: 'PUT', - uri: url, - jar: this.cookies, - json: data, - headers: { - ...this.headers, - 'Content-Type': 'application/json' - } - }; - const { body } = await this.scraper(options); - return asJson(body); - } -} diff --git a/src/common/DateUtils.ts b/src/common/DateUtils.ts deleted file mode 100644 index 2716d00..0000000 --- a/src/common/DateUtils.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function toDateString(date: Date) { - const offset = date.getTimezoneOffset(); - const offsetDate = new Date(date.getTime() - offset * 60 * 1000); - const [dateString] = offsetDate.toISOString().split('T'); - return dateString; -} diff --git a/src/common/HttpClient.ts b/src/common/HttpClient.ts new file mode 100644 index 0000000..69fab97 --- /dev/null +++ b/src/common/HttpClient.ts @@ -0,0 +1,401 @@ +import axios, { + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + RawAxiosRequestHeaders +} from 'axios'; +import FormData from 'form-data'; +import _ from 'lodash'; +import { DateTime } from 'luxon'; +import OAuth from 'oauth-1.0a'; +import qs from 'qs'; +import { UrlClass } from '../garmin/UrlClass'; +import { + IOauth1, + IOauth1Consumer, + IOauth1Token, + IOauth2Token +} from '../garmin/types'; +const crypto = require('crypto'); + +const CSRF_RE = new RegExp('name="_csrf"\\s+value="(.+?)"'); +const TICKET_RE = new RegExp('ticket=([^"]+)"'); +const ACCOUNT_LOCKED_RE = new RegExp('var statuss*=s*"([^"]*)"'); +const PAGE_TITLE_RE = new RegExp('([^<]*)'); + +const USER_AGENT_CONNECTMOBILE = 'com.garmin.android.apps.connectmobile'; +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'; + +const OAUTH_CONSUMER_URL = + 'https://thegarth.s3.amazonaws.com/oauth_consumer.json'; +// refresh token +let isRefreshing = false; +let refreshSubscribers: ((token: string) => void)[] = []; + +export class HttpClient { + client: AxiosInstance; + url: UrlClass; + oauth1Token: IOauth1Token | undefined; + oauth2Token: IOauth2Token | undefined; + OAUTH_CONSUMER: IOauth1Consumer | undefined; + + constructor(url: UrlClass) { + this.url = url; + this.client = axios.create(); + this.client.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + // console.log('originalRequest:', originalRequest) + // Auto Refresh token + if ( + error?.response?.status === 401 && + !originalRequest?._retry + ) { + if (!this.oauth2Token) { + return; + } + if (isRefreshing) { + try { + const token = await new Promise( + (resolve) => { + refreshSubscribers.push((token) => { + resolve(token); + }); + } + ); + originalRequest.headers.Authorization = `Bearer ${token}`; + return this.client(originalRequest); + } catch (err) { + console.log('err:', err); + return Promise.reject(err); + } + } + + originalRequest._retry = true; + isRefreshing = true; + console.log('interceptors: refreshOauth2Token start'); + await this.refreshOauth2Token(); + console.log('interceptors: refreshOauth2Token end'); + isRefreshing = false; + refreshSubscribers.forEach((subscriber) => + subscriber(this.oauth2Token!.access_token) + ); + refreshSubscribers = []; + originalRequest.headers.Authorization = `Bearer ${ + this.oauth2Token!.access_token + }`; + return this.client(originalRequest); + } + if (axios.isAxiosError(error)) { + if (error?.response) this.handleError(error?.response); + } + throw error; + } + ); + this.client.interceptors.request.use(async (config) => { + if (this.oauth2Token) { + config.headers.Authorization = + 'Bearer ' + this.oauth2Token.access_token; + } + return config; + }); + } + + async fetchOauthConsumer() { + const response = await axios.get(OAUTH_CONSUMER_URL); + this.OAUTH_CONSUMER = { + key: response.data.consumer_key, + secret: response.data.consumer_secret + }; + } + + async checkTokenVaild() { + if (this.oauth2Token) { + if (this.oauth2Token.expires_at < DateTime.now().toSeconds()) { + console.error('Token expired!'); + await this.refreshOauth2Token(); + } + } + } + + async get(url: string, config?: AxiosRequestConfig): Promise { + const response = await this.client.get(url, config); + return response?.data; + } + + async post( + url: string, + data: any, + config?: AxiosRequestConfig + ): Promise { + const response = await this.client.post(url, data, config); + return response?.data; + } + + async delete(url: string, config?: AxiosRequestConfig): Promise { + const response = await this.client.post(url, null, { + ...config, + headers: { + ...config?.headers, + 'X-Http-Method-Override': 'DELETE' + } + }); + return response?.data; + } + + setCommonHeader(headers: RawAxiosRequestHeaders): void { + _.each(headers, (headerValue, key) => { + this.client.defaults.headers.common[key] = headerValue; + }); + } + + handleError(response: AxiosResponse): void { + this.handleHttpError(response); + } + + handleHttpError(response: AxiosResponse): void { + const { status, statusText, data } = response; + const msg = `ERROR: (${status}), ${statusText}, ${JSON.stringify( + data + )}`; + console.error(msg); + throw new Error(msg); + } + + /** + * Login to Garmin Connect + * @param username + * @param password + * @returns {Promise} + */ + async login(username: string, password: string): Promise { + await this.fetchOauthConsumer(); + // Step1-3: Get ticket from page. + const ticket = await this.getLoginTicket(username, password); + // Step4: Oauth1 + const oauth1 = await this.getOauth1Token(ticket); + // TODO: Handle MFA + + // Step 5: Oauth2 + await this.exchange(oauth1); + return this; + } + + private async getLoginTicket( + username: string, + password: string + ): Promise { + // Step1: Set cookie + const step1Params = { + clientId: 'GarminConnect', + locale: 'en', + service: this.url.GC_MODERN + }; + const step1Url = `${this.url.GARMIN_SSO_EMBED}?${qs.stringify( + step1Params + )}`; + // console.log('login - step1Url:', step1Url); + await this.client.get(step1Url); + + // Step2 Get _csrf + const step2Params = { + id: 'gauth-widget', + embedWidget: true, + locale: 'en', + gauthHost: this.url.GARMIN_SSO_EMBED + }; + const step2Url = `${this.url.SIGNIN_URL}?${qs.stringify(step2Params)}`; + // console.log('login - step2Url:', step2Url); + const step2Result = await this.get(step2Url); + // console.log('login - step2Result:', step2Result) + const csrfRegResult = CSRF_RE.exec(step2Result); + if (!csrfRegResult) { + throw new Error('login - csrf not found'); + } + const csrf_token = csrfRegResult[1]; + // console.log('login - csrf:', csrf_token); + + // Step3 Get ticket + const signinParams = { + id: 'gauth-widget', + embedWidget: true, + clientId: 'GarminConnect', + locale: 'en', + gauthHost: this.url.GARMIN_SSO_EMBED, + service: this.url.GARMIN_SSO_EMBED, + source: this.url.GARMIN_SSO_EMBED, + redirectAfterAccountLoginUrl: this.url.GARMIN_SSO_EMBED, + redirectAfterAccountCreationUrl: this.url.GARMIN_SSO_EMBED + }; + const step3Url = `${this.url.SIGNIN_URL}?${qs.stringify(signinParams)}`; + // console.log('login - step3Url:', step3Url); + const step3Form = new FormData(); + step3Form.append('username', username); + step3Form.append('password', password); + step3Form.append('embed', 'true'); + step3Form.append('_csrf', csrf_token); + const step3Result = await this.post(step3Url, step3Form, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Dnt: 1, + Origin: this.url.GARMIN_SSO_ORIGIN, + Referer: this.url.SIGNIN_URL, + 'User-Agent': USER_AGENT_BROWSER + } + }); + // console.log('step3Result:', step3Result) + this.handleAccountLocked(step3Result); + this.handlePageTitle(step3Result); + this.handleMFA(step3Result); + + const ticketRegResult = TICKET_RE.exec(step3Result); + if (!ticketRegResult) { + throw new Error( + 'login failed (Ticket not found or MFA), please check username and password' + ); + } + const ticket = ticketRegResult[1]; + return ticket; + } + + // TODO: Handle MFA + handleMFA(htmlStr: string): void {} + + // TODO: Handle Phone number + handlePageTitle(htmlStr: string): void { + const pageTitileRegResult = PAGE_TITLE_RE.exec(htmlStr); + if (pageTitileRegResult) { + const title = pageTitileRegResult[1]; + console.log('login page title:', title); + if (_.includes(title, 'Update Phone Number')) { + // current I don't know where to update it + // See: https://github.com/matin/garth/issues/19 + throw new Error( + "login failed (Update Phone number), please update your phone number, currently I don't know where to update it" + ); + } + } + } + + handleAccountLocked(htmlStr: string): void { + const accountLockedRegResult = ACCOUNT_LOCKED_RE.exec(htmlStr); + if (accountLockedRegResult) { + const msg = accountLockedRegResult[1]; + console.error(msg); + throw new Error( + 'login failed (AccountLocked), please open connect web page to unlock your account' + ); + } + } + + async refreshOauth2Token() { + if (!this.OAUTH_CONSUMER) { + await this.fetchOauthConsumer(); + } + if (!this.oauth2Token || !this.oauth1Token) { + throw new Error('No Oauth2Token or Oauth1Token'); + } + const oauth1 = { + oauth: this.getOauthClient(this.OAUTH_CONSUMER!), + token: this.oauth1Token + }; + await this.exchange(oauth1); + console.log('Oauth2 token refreshed!'); + } + + async getOauth1Token(ticket: string): Promise { + if (!this.OAUTH_CONSUMER) { + throw new Error('No OAUTH_CONSUMER'); + } + const params = { + ticket, + 'login-url': this.url.GARMIN_SSO_EMBED, + 'accepts-mfa-tokens': true + }; + const url = `${this.url.OAUTH_URL}/preauthorized?${qs.stringify( + params + )}`; + + const oauth = this.getOauthClient(this.OAUTH_CONSUMER); + + const step4RequestData = { + url: url, + method: 'GET' + }; + const headers = oauth.toHeader(oauth.authorize(step4RequestData)); + // console.log('getOauth1Token - headers:', headers); + + const response = await this.get(url, { + headers: { + ...headers, + 'User-Agent': USER_AGENT_CONNECTMOBILE + } + }); + // console.log('getOauth1Token - response:', response); + const token = qs.parse(response) as unknown as IOauth1Token; + // console.log('getOauth1Token - token:', token); + this.oauth1Token = token; + return { token, oauth }; + } + + getOauthClient(consumer: IOauth1Consumer): OAuth { + const oauth = new OAuth({ + consumer: consumer, + signature_method: 'HMAC-SHA1', + hash_function(base_string: string, key: string) { + return crypto + .createHmac('sha1', key) + .update(base_string) + .digest('base64'); + } + }); + return oauth; + } + // + async exchange(oauth1: IOauth1) { + const token = { + key: oauth1.token.oauth_token, + secret: oauth1.token.oauth_token_secret + }; + // console.log('exchange - token:', token); + + const baseUrl = `${this.url.OAUTH_URL}/exchange/user/2.0`; + const requestData = { + url: baseUrl, + method: 'POST', + data: null + }; + + const step5AuthData = oauth1.oauth.authorize(requestData, token); + // console.log('login - step5AuthData:', step5AuthData); + const url = `${baseUrl}?${qs.stringify(step5AuthData)}`; + // console.log('exchange - url:', url); + this.oauth2Token = undefined; + const response = await this.post(url, null, { + headers: { + 'User-Agent': USER_AGENT_CONNECTMOBILE, + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + // console.log('exchange - response:', response); + this.oauth2Token = this.setOauth2TokenExpiresAt(response); + // console.log('exchange - oauth2Token:', this.oauth2Token); + } + + setOauth2TokenExpiresAt(token: IOauth2Token): IOauth2Token { + // human readable date + token['last_update_date'] = DateTime.now().toLocal().toString(); + token['expires_date'] = DateTime.fromSeconds( + DateTime.now().toSeconds() + token['expires_in'] + ) + .toLocal() + .toString(); + // timestamp for check expired + token['expires_at'] = DateTime.now().toSeconds() + token['expires_in']; + token['refresh_token_expires_at'] = + DateTime.now().toSeconds() + token['refresh_token_expires_in']; + return token; + } +} diff --git a/src/garmin/GarminConnect.ts b/src/garmin/GarminConnect.ts index 03c052a..4bb5190 100644 --- a/src/garmin/GarminConnect.ts +++ b/src/garmin/GarminConnect.ts @@ -1,24 +1,31 @@ import appRoot from 'app-root-path'; -import CFClient from '../common/CFClient'; -import { toDateString } from '../common/DateUtils'; -import * as urls from './Urls'; -import { ExportFileType, UploadFileType } from './Urls'; -import { CookieJar } from 'tough-cookie'; + +import FormData from 'form-data'; +import _ from 'lodash'; +import { DateTime } from 'luxon'; +import * as fs from 'fs'; +import * as path from 'path'; +import { HttpClient } from '../common/HttpClient'; +import { checkIsDirectory, createDirectory, writeToFile } from '../utils'; +import { UrlClass } from './UrlClass'; import { + ExportFileTypeValue, GCActivityId, - GCBadgeId, GCUserHash, - Gear, + GarminDomain, IActivity, - IActivityDetails, - IBadge, - ISocialConnections, + ICountActivities, + IGarminTokens, + IOauth1Token, + IOauth2Token, ISocialProfile, - IUserInfo + IUserSettings, + IWorkout, + IWorkoutDetail, + UploadFileType, + UploadFileTypeTypeValue } from './types'; import Running from './workouts/Running'; -import path from 'path'; -import fs from 'fs'; let config: GCCredentials | undefined = undefined; @@ -34,7 +41,6 @@ export interface GCCredentials { username: string; password: string; } - export interface Listeners { [event: string]: EventCallback[]; } @@ -43,585 +49,255 @@ export enum Event { sessionChange = 'sessionChange' } -export interface Session { - cookies: CookieJar.Serialized | undefined; - userHash: string | undefined; -} +export interface Session {} export default class GarminConnect { - private client: CFClient; + client: HttpClient; private _userHash: GCUserHash | undefined; private credentials: GCCredentials; private listeners: Listeners; - - constructor(credentials: GCCredentials | undefined = config) { - const headers = { - origin: urls.GARMIN_SSO_ORIGIN, - nk: 'NT' - }; - this.client = new CFClient(headers); - this._userHash = undefined; + private url: UrlClass; + // private oauth1: OAuth; + constructor( + credentials: GCCredentials | undefined = config, + domain: GarminDomain = 'garmin.com' + ) { if (!credentials) { throw new Error('Missing credentials'); } this.credentials = credentials; + this.url = new UrlClass(domain); + this.client = new HttpClient(this.url); + this._userHash = undefined; this.listeners = {}; } - get userHash(): GCUserHash { - if (!this._userHash) { - throw new Error('User not logged in'); - } - return this._userHash; - } - - get sessionJson(): Session { - const cookies = this.client.serializeCookies(); - return { cookies, userHash: this._userHash }; - } - - set sessionJson(json: Session) { - const { cookies, userHash } = json || {}; - if (cookies && userHash) { - this._userHash = userHash; - this.client.importCookies(cookies); - } - } - - /** - * Add an event listener callback - * @param event - * @param callback - */ - on(event: Event, callback: EventCallback) { - if ( - event && - callback && - typeof event === 'string' && - typeof callback === 'function' - ) { - if (!this.listeners[event]) { - this.listeners[event] = []; - } - this.listeners[event].push(callback); - } - } - - /** - * Method for triggering any event - * @param event - * @param data - */ - triggerEvent(event: Event, data: T) { - const callbacks = this.listeners[event] || []; - callbacks.forEach((cb) => cb(data)); - } - - /** - * Add a callback to the 'sessionChange' event - * @param callback - */ - onSessionChange(callback: EventCallback) { - this.on(Event.sessionChange, callback); - } - - /** - * Restore an old session from storage and fallback to regular login - * @param json - * @param username - * @param password - * @returns {Promise} - */ - async restoreOrLogin(json: Session, username: string, password: string) { - return this.restore(json).catch((e) => { - console.warn(e); - return this.login(username, password); - }); - } - - /** - * Restore an old session from storage - * @param json - * @returns {Promise} - */ - async restore(json: Session) { - this.sessionJson = json; - try { - const info = await this.getUserInfo(); - const { displayName } = info || {}; - if (displayName && displayName === this.userHash) { - // Session restoration was successful - return this; - } - throw new Error( - 'Unable to restore session, user hash do not match' - ); - } catch (e) { - throw new Error(`Unable to restore session due to: ${e}`); - } - } - - /** - * Login to Garmin Connect - * @param username - * @param password - * @returns {Promise<*>} - */ async login(username?: string, password?: string): Promise { if (username && password) { this.credentials.username = username; this.credentials.password = password; } - let tempCredentials = { - ...this.credentials, - rememberme: 'on', - embed: 'false' - }; - await this.client.get(urls.SIGNIN_URL); - await this.client.post(urls.SIGNIN_URL, tempCredentials); - const userPreferences = await this.getUserInfo(); - const { displayName } = userPreferences; - this._userHash = displayName; - return this; - } - - // User info - /** - * Get basic user information - * @returns {Promise<*>} - */ - async getUserInfo(): Promise { - return this.get(urls.userInfo()); - } - - /** - * Get social user information - * @returns {Promise<*>} - */ - async getSocialProfile(): Promise { - return this.get(urls.socialProfile(this.userHash)); - } - - /** - * Get a list of all social connections - * @returns {Promise<*>} - */ - async getSocialConnections(): Promise { - return this.get( - urls.socialConnections(this.userHash) + await this.client.login( + this.credentials.username, + this.credentials.password ); + return this; } - - // Devices - /** - * Get a list of all registered devices - * @returns {Promise<*>} - */ - async getDeviceInfo() { - return this.get(urls.deviceInfo(this.userHash)); - } - - // Sleep data - /** - * Get detailed sleep data for a specific date - * @param date - * @returns {Promise<*>} - */ - async getSleepData(date = new Date()) { - const dateString = toDateString(date); - return this.get(urls.dailySleepData(this.userHash), { - date: dateString - }); - } - - /** - * Get sleep data summary for a specific date - * @param date - * @returns {Promise<*>} - */ - async getSleep(date = new Date()) { - const dateString = toDateString(date); - return this.get(urls.dailySleep(), { date: dateString }); - } - - // Heart rate - /** - * Get heart rate measurements for a specific date - * @param date - * @returns {Promise<*>} - */ - async getHeartRate(date = new Date()) { - const dateString = toDateString(date); - return this.get(urls.dailyHeartRate(this.userHash), { - date: dateString - }); - } - - // Weight - /** - * Post a new body weight - * @param weight - * @returns {Promise<*>} - */ - async setBodyWeight(weight: number) { - if (weight) { - const roundWeight = Math.round(weight * 1000); - const data = { userData: { weight: roundWeight } }; - return this.put(urls.userSettings(), data); + exportTokenToFile(dirPath: string): void { + if (!checkIsDirectory(dirPath)) { + createDirectory(dirPath); } - return Promise.reject(); - } - - // Activites - /** - * Get list of activites - * @param start - * @param limit - * @returns {Promise<*>} - */ - async getActivities(start: number, limit: number): Promise { - return this.get(urls.activities(), { start, limit }); - } - - /** - * Get details about an activity - * @param activityId - * @returns {Promise} - */ - async getActivityDetails( - activityId: GCActivityId - ): Promise { - if (activityId) { - return this.get(urls.activity(activityId)); + // save oauth1 to json + if (this.client.oauth1Token) { + writeToFile( + path.join(dirPath, 'oauth1_token.json'), + JSON.stringify(this.client.oauth1Token) + ); } - return Promise.reject(); - } - - /** - * Get metrics details about an activity - * @param activity - * @param maxChartSize - * @param maxPolylineSize - * @returns {Promise<*>} - */ - async getActivity( - activity: { activityId: GCActivityId }, - maxChartSize: number, - maxPolylineSize: number - ) { - const { activityId } = activity || {}; - if (activityId) { - return this.get(urls.activityDetails(activityId), { - maxChartSize, - maxPolylineSize - }); + if (this.client.oauth2Token) { + writeToFile( + path.join(dirPath, 'oauth2_token.json'), + JSON.stringify(this.client.oauth2Token) + ); } - return Promise.reject(); } - - /** - * Get weather data from an activity - * @param activity - * @returns {Promise<*>} - */ - async getActivityWeather(activity: { activityId: GCActivityId }) { - const { activityId } = activity || {}; - if (activityId) { - return this.get(urls.weather(activityId)); + loadTokenByFile(dirPath: string): void { + if (!checkIsDirectory(dirPath)) { + throw new Error('loadTokenByFile: Directory not found: ' + dirPath); + } + let oauth1Data = fs.readFileSync( + path.join(dirPath, 'oauth1_token.json') + ) as unknown as string; + const oauth1 = JSON.parse(oauth1Data); + this.client.oauth1Token = oauth1; + + let oauth2Data = fs.readFileSync( + path.join(dirPath, 'oauth2_token.json') + ) as unknown as string; + const oauth2 = JSON.parse(oauth2Data); + this.client.oauth2Token = oauth2; + } + exportToken(): IGarminTokens { + if (!this.client.oauth1Token || !this.client.oauth2Token) { + throw new Error('exportToken: Token not found'); } - return Promise.reject(); + return { + oauth1: this.client.oauth1Token, + oauth2: this.client.oauth2Token + }; } - - /** - * Updates an activity - * @param activity - * @returns {Promise<*>} - */ - async updateActivity(activity: { activityId: GCActivityId }) { - return this.put(urls.activity(activity.activityId), activity); + // from db or localstorage etc + loadToken(oauth1: IOauth1Token, oauth2: IOauth2Token): void { + this.client.oauth1Token = oauth1; + this.client.oauth2Token = oauth2; } - /** - * Deletes an activity - * @param activity - * @returns {Promise<*>} - */ - async deleteActivity(activity: { activityId: GCActivityId }) { - const { activityId } = activity || {}; - if (activityId) { - const headers = { 'x-http-method-override': 'DELETE' }; - return this.client.postJson( - urls.activity(activityId), - undefined, - headers - ); - } - return Promise.reject(); + async getUserSettings(): Promise { + return this.client.get(this.url.USER_SETTINGS); } - /** - * Get list of activities in your news feed - * @param start - * @param limit - * @returns {Promise<*>} - */ - async getNewsFeed(start: number, limit: number) { - return this.get(urls.newsFeed(), { start, limit }); + async getUserProfile(): Promise { + return this.client.get(this.url.USER_PROFILE); } - // Steps - /** - * Get step count for a specific date - * @param date - * @returns {Promise<*>} - */ - async getSteps(date = new Date()) { - const dateString = toDateString(date); - return this.get(urls.dailySummaryChart(this.userHash), { - date: dateString + async getActivities(start: number, limit: number): Promise { + return this.client.get(this.url.ACTIVITIES, { + params: { start, limit } }); } - - // Workouts - /** - * Get list of workouts - * @param start - * @param limit - * @returns {Promise<*>} - */ - async getWorkouts(start: number, limit: number) { - return this.get(urls.workouts(), { start, limit }); + async getActivity(activity: { + activityId: GCActivityId; + }): Promise { + if (!activity.activityId) throw new Error('Missing activityId'); + return this.client.get( + this.url.ACTIVITY + activity.activityId + ); + } + async countActivities(): Promise { + return this.client.get(this.url.STAT_ACTIVITIES, { + params: { + aggregation: 'lifetime', + startDate: '1970-01-01', + endDate: DateTime.now().toFormat('yyyy-MM-dd'), + metric: 'duration' + } + }); } - /** - * Download original activity data to disk as zip - * Resolves to absolute path for the downloaded file - * @param activity : any - * @param dir Will default to current working directory - * @param type : string - Will default to 'zip'. Other possible values are 'tcx', 'gpx' or 'kml'. - * @returns {Promise<*>} - */ async downloadOriginalActivityData( activity: { activityId: GCActivityId }, dir: string, - type?: ExportFileType - ) { - const { activityId } = activity || {}; - if (activityId) { - const url = - !type || type === ExportFileType.zip - ? urls.originalFile(activityId) - : urls.exportFile(activityId, type); - return this.client.downloadBlob(dir, url); + type: ExportFileTypeValue = 'zip' + ): Promise { + if (!activity.activityId) throw new Error('Missing activityId'); + if (!checkIsDirectory(dir)) { + createDirectory(dir); } - return Promise.reject(); + let fileBuffer: Buffer; + if (type === 'tcx') { + fileBuffer = await this.client.get( + this.url.DOWNLOAD_TCX + activity.activityId + ); + } else if (type === 'gpx') { + fileBuffer = await this.client.get( + this.url.DOWNLOAD_GPX + activity.activityId + ); + } else if (type === 'kml') { + fileBuffer = await this.client.get( + this.url.DOWNLOAD_KML + activity.activityId + ); + } else if (type === 'zip') { + fileBuffer = await this.client.get( + this.url.DOWNLOAD_ZIP + activity.activityId, + { + responseType: 'arraybuffer' + } + ); + } else { + throw new Error( + 'downloadOriginalActivityData - Invalid type: ' + type + ); + } + writeToFile( + path.join(dir, `${activity.activityId}.${type}`), + fileBuffer + ); } - /** - * Uploads an activity file ('gpx', 'tcx', or 'fit') - * @param file the file to upload - * @param format the format of the file. If undefined, the extension of the file will be used. - * @returns {Promise<*>} - */ - async uploadActivity(file: string, format: UploadFileType) { + async uploadActivity( + file: string, + format: UploadFileTypeTypeValue = 'fit' + ) { const detectedFormat = (format || path.extname(file))?.toLowerCase(); - const filename = path.basename(file); - - if ((Object).values(UploadFileType).includes(detectedFormat)) { - return Promise.reject(); + if (!_.includes(UploadFileType, detectedFormat)) { + throw new Error('uploadActivity - Invalid format: ' + format); } - const fileBuffer = fs.readFileSync(file); - const response = this.client.post(urls.upload(format), { - userfile: { - value: fileBuffer, - options: { - filename + const fileBuffer = fs.createReadStream(file); + const form = new FormData(); + form.append('userfile', fileBuffer); + const response = await this.client.post( + this.url.UPLOAD + '.' + format, + form, + { + headers: { + 'Content-Type': form.getHeaders()['content-type'] } } - }); + ); return response; } - /** - * Adds a running workout with one step of completeing a set distance. - * @param name - * @param meters - * @param description - * @returns {Promise<*>} - */ - async addRunningWorkout(name: string, meters: number, description: string) { - const running = new Running(); - running.name = name; - running.distance = meters; - running.description = description; - return this.addWorkout(running); - } - - /** - * Add a new workout preset. - * @param workout - * @returns {Promise<*>} - */ - async addWorkout(workout: any) { - if (workout.isValid()) { - const data = { ...workout.toJson() }; - if (!data.description) { - data.description = 'Added by garmin-connect for Node.js'; - } - return this.post(urls.workout(), data); - } - return Promise.reject(); - } - - /** - * Add a workout to your workout calendar. - * @param workout - * @param date - * @returns {Promise<*>} - */ - async scheduleWorkout(workout: any, date: Date) { - const { workoutId } = workout || {}; - if (workoutId && date) { - const dateString = toDateString(date); - return this.post(urls.schedule(workoutId), { date: dateString }); - } - return Promise.reject(); + async deleteActivity(activity: { + activityId: GCActivityId; + }): Promise { + if (!activity.activityId) throw new Error('Missing activityId'); + await this.client.delete(this.url.ACTIVITY + activity.activityId); } - /** - * Delete a workout based on a workout object. - * @param workout - * @returns {Promise<*>} - */ - async deleteWorkout(workout: any) { - const { workoutId } = workout || {}; - if (workoutId) { - const headers = { 'x-http-method-override': 'DELETE' }; - return this.client.postJson( - urls.workout(workoutId), - undefined, - headers - ); - } - return Promise.reject(); - } - - // Badges - /** - * Get list of earned badges - * @returns {Promise<*>} - */ - async getBadgesEarned(): Promise { - return this.get(urls.badgesEarned()); - } - - /** - * Get list of available badges - * @returns {Promise<*>} - */ - async getBadgesAvailable(): Promise { - return this.get(urls.badgesAvailable()); - } - - /** - * Get details about an badge - * @param badge - * @returns {Promise<*>} - */ - async getBadge(badge: { badgeId: GCBadgeId }) { - const { badgeId } = badge || {}; - if (badgeId) { - return this.get(urls.badgeDetail(badgeId)); - } - return Promise.reject(); - } - - /** - * Uploads an image to an activity - * @param activity - * @param file the file to upload - * @returns {Promise<*>} - */ - async uploadImage(activity: { activityId: GCActivityId }, file: string) { - return this.client.post(urls.image(activity.activityId), { - file: { - value: fs.readFileSync(file), - options: { - filename: path.basename(file) - } + async getWorkouts(start: number, limit: number): Promise { + return this.client.get(this.url.WORKOUTS, { + params: { + start, + limit } }); } - - /** - * Delete an image from an activity - * @param activity - * @param imageId, can be found in `activityImages` array of the activity - * @returns {Promise} - */ - async deleteImage( - activity: { activityId: GCActivityId }, - imageId: string - ): Promise { - return this.client.delete( - urls.imageDelete(activity.activityId, imageId) + async getWorkoutDetail(workout: { + workoutId: string; + }): Promise { + if (!workout.workoutId) throw new Error('Missing workoutId'); + return this.client.get( + this.url.WORKOUT(workout.workoutId) ); } - /** - * List the gear available at a certain date - * @param userProfilePk, user profile private key (can be found in user or activity details) - * @param availableGearDate, list gear available at this date only - * @returns {Promise} - */ - async listGear( - userProfilePk: number, - availableGearDate?: Date - ): Promise { - return this.client.get(urls.listGear(userProfilePk, availableGearDate)); - } - - /** - * Link gear to activity - * @param activityId, Activity ID - * @param gearUuid, UUID of the gear - * @returns {Promise} - */ - async linkGear(activityId: GCActivityId, gearUuid: string): Promise { - return this.put(urls.linkGear(activityId, gearUuid), {}); - } - - /** - * Unlink gear to activity - * @param activityId, Activity ID - * @param gearUuid, UUID of the gear - * @returns {Promise} - */ - async unlinkGear( - activityId: GCActivityId, - gearUuid: string - ): Promise { - return this.put(urls.unlinkGear(activityId, gearUuid), {}); - } + async addWorkout( + workout: IWorkoutDetail | Running + ): Promise { + if (!workout) throw new Error('Missing workout'); - // General methods + if (workout instanceof Running) { + if (workout.isValid()) { + const data = { ...workout.toJson() }; + if (!data.description) { + data.description = 'Added by garmin-connect for Node.js'; + } + return this.client.post( + this.url.WORKOUT(), + data + ); + } + } - async get(url: string, data?: any) { - const response = await this.client.get(url, data); - this.triggerEvent(Event.sessionChange, this.sessionJson); - return response as T; + const newWorkout = _.omit(workout, [ + 'workoutId', + 'ownerId', + 'updatedDate', + 'createdDate', + 'author' + ]); + if (!newWorkout.description) { + newWorkout.description = 'Added by garmin-connect for Node.js'; + } + // console.log('addWorkout - newWorkout:', newWorkout) + return this.client.post(this.url.WORKOUT(), newWorkout); } - async post(url: string, data: any) { - const response = await this.client.postJson(url, data, {}); - this.triggerEvent(Event.sessionChange, this.sessionJson); - return response as T; + async addRunningWorkout( + name: string, + meters: number, + description: string + ): Promise { + const running = new Running(); + running.name = name; + running.distance = meters; + running.description = description; + return this.addWorkout(running); } - async put(url: string, data: any) { - const response = await this.client.putJson(url, data); - this.triggerEvent(Event.sessionChange, this.sessionJson); - return response as T; + async deleteWorkout(workout: { workoutId: string }) { + if (!workout.workoutId) throw new Error('Missing workout'); + return this.client.delete(this.url.WORKOUT(workout.workoutId)); } } diff --git a/src/garmin/UrlClass.ts b/src/garmin/UrlClass.ts new file mode 100644 index 0000000..d5394b1 --- /dev/null +++ b/src/garmin/UrlClass.ts @@ -0,0 +1,74 @@ +import { GCWorkoutId, GarminDomain } from './types'; + +export class UrlClass { + private domain: GarminDomain; + GC_MODERN: string; + GARMIN_SSO_ORIGIN: string; + GC_API: string; + constructor(domain: GarminDomain = 'garmin.com') { + this.domain = domain; + this.GC_MODERN = `https://connect.${this.domain}/modern`; + this.GARMIN_SSO_ORIGIN = `https://sso.${this.domain}`; + this.GC_API = `https://connectapi.${this.domain}`; + } + get GARMIN_SSO() { + return `${this.GARMIN_SSO_ORIGIN}/sso`; + } + get GARMIN_SSO_EMBED() { + return `${this.GARMIN_SSO_ORIGIN}/sso/embed`; + } + get BASE_URL() { + return `${this.GC_MODERN}/proxy`; + } + get SIGNIN_URL() { + return `${this.GARMIN_SSO}/signin`; + } + get LOGIN_URL() { + return `${this.GARMIN_SSO}/login`; + } + get OAUTH_URL() { + return `${this.GC_API}/oauth-service/oauth`; + } + get USER_SETTINGS() { + return `${this.GC_API}/userprofile-service/userprofile/user-settings/`; + } + get USER_PROFILE() { + return `${this.GC_API}/userprofile-service/socialProfile`; + } + get ACTIVITIES() { + return `${this.GC_API}/activitylist-service/activities/search/activities`; + } + get ACTIVITY() { + return `${this.GC_API}/activity-service/activity/`; + } + get STAT_ACTIVITIES() { + return `${this.GC_API}/fitnessstats-service/activity`; + } + get DOWNLOAD_ZIP() { + return `${this.GC_API}/download-service/files/activity/`; + } + get DOWNLOAD_GPX() { + return `${this.GC_API}/download-service/export/gpx/activity/`; + } + get DOWNLOAD_TCX() { + return `${this.GC_API}/download-service/export/tcx/activity/`; + } + get DOWNLOAD_KML() { + return `${this.GC_API}/download-service/export/kml/activity/`; + } + get UPLOAD() { + return `${this.GC_API}/upload-service/upload/`; + } + get IMPORT_DATA() { + return `${this.GC_API}/modern/import-data`; + } + WORKOUT(id?: GCWorkoutId) { + if (id) { + return `${this.GC_API}/workout-service/workout/${id}`; + } + return `${this.GC_API}/workout-service/workout`; + } + get WORKOUTS() { + return `${this.GC_API}/workout-service/workouts`; + } +} diff --git a/src/garmin/Urls.ts b/src/garmin/Urls.ts deleted file mode 100644 index 39de2aa..0000000 --- a/src/garmin/Urls.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { GCActivityId, GCBadgeId, GCUserHash, GCWorkoutId } from './types'; - -export const GC_MODERN = 'https://connect.garmin.com/modern'; -export const GARMIN_SSO_ORIGIN = 'https://sso.garmin.com'; -export const GARMIN_SSO = `${GARMIN_SSO_ORIGIN}/sso`; -export const BASE_URL = `${GC_MODERN}/proxy`; -export const SIGNIN_URL = `${GARMIN_SSO}/signin`; -export const LOGIN_URL = `${GARMIN_SSO}/login`; - -export const ACTIVITY_SERVICE = `${BASE_URL}/activity-service`; -export const ACTIVITYLIST_SERVICE = `${BASE_URL}/activitylist-service`; -export const BADGE_SERVICE = `${BASE_URL}/badge-service`; -export const CURRENT_USER_SERVICE = `${GC_MODERN}/currentuser-service/user/info`; -export const DEVICE_SERVICE = `${BASE_URL}/device-service`; -export const DOWNLOAD_SERVICE = `${BASE_URL}/download-service`; -export const USERPROFILE_SERVICE = `${BASE_URL}/userprofile-service`; -export const WELLNESS_SERVICE = `${BASE_URL}/wellness-service`; -export const WORKOUT_SERVICE = `${BASE_URL}/workout-service`; -export const UPLOAD_SERVICE = `${BASE_URL}/upload-service`; -export const GEAR_SERVICE = `${BASE_URL}/gear-service`; - -export const USER_SETTINGS = `${USERPROFILE_SERVICE}/userprofile/user-settings/`; - -export enum ExportFileType { - tcx = 'tcx', - gpx = 'gpx', - kml = 'kml', - zip = 'zip' -} - -export enum UploadFileType { - tcx = 'tcx', - gpx = 'gpx', - fit = 'fit' -} - -export const activity = (id: GCActivityId) => - `${ACTIVITY_SERVICE}/activity/${id}`; - -export const image = (id: GCActivityId) => - `${ACTIVITY_SERVICE}/activity/${id}/image`; - -export const imageDelete = (id: GCActivityId, imageId: string) => - `${ACTIVITY_SERVICE}/activity/${id}/image/${imageId}`; - -export const weather = (id: GCActivityId) => `${activity(id)}/weather`; - -export const activityDetails = (id: GCActivityId) => `${activity(id)}/details`; - -export const activities = () => - `${ACTIVITYLIST_SERVICE}/activities/search/activities`; - -export const badgesAvailable = () => `${BADGE_SERVICE}/badge/available`; - -export const badgeDetail = (id: GCBadgeId) => - `${BADGE_SERVICE}/badge/detail/v2/${id}`; - -export const badgesEarned = () => `${BADGE_SERVICE}/badge/earned`; - -export const dailyHeartRate = (userHash: GCUserHash) => - `${WELLNESS_SERVICE}/wellness/dailyHeartRate/${userHash}`; - -export const dailySleep = () => `${WELLNESS_SERVICE}/wellness/dailySleep`; - -export const dailySleepData = (userHash: GCUserHash) => - `${WELLNESS_SERVICE}/wellness/dailySleepData/${userHash}`; - -export const dailySummaryChart = (userHash: GCUserHash) => - `${WELLNESS_SERVICE}/wellness/dailySummaryChart/${userHash}`; - -export const deviceInfo = (userHash: GCUserHash) => - `${DEVICE_SERVICE}/deviceservice/device-info/all/${userHash}`; - -export const schedule = (id: GCActivityId) => - `${WORKOUT_SERVICE}/schedule/${id}`; - -export const userInfo = () => CURRENT_USER_SERVICE; - -export const socialProfile = (userHash: GCUserHash) => - `${USERPROFILE_SERVICE}/socialProfile/${userHash}`; - -export const userSettings = () => USER_SETTINGS; - -export const originalFile = (id: GCActivityId) => - `${DOWNLOAD_SERVICE}/files/activity/${id}`; - -/** - * - * @param id {string} - * @param type "tcx" | "gpx" | "kml" - * @return {`${string}/export/${string}/activity/${string}`} - */ -export const exportFile = (id: GCActivityId, type: ExportFileType) => - `${DOWNLOAD_SERVICE}/export/${type}/activity/${id}`; - -export const workout = (id?: GCWorkoutId) => { - if (id) { - return `${WORKOUT_SERVICE}/workout/${id}`; - } - return `${WORKOUT_SERVICE}/workout`; -}; - -export const workouts = () => `${WORKOUT_SERVICE}/workouts`; - -export const socialConnections = (userHash: GCUserHash) => - `${USERPROFILE_SERVICE}/socialProfile/connections/${userHash}`; - -export const newsFeed = () => - `${ACTIVITYLIST_SERVICE}/activities/subscriptionFeed`; - -export const upload = (format: UploadFileType) => - `${UPLOAD_SERVICE}/upload/${format}`; - -export const listGear = (userProfilePk: number, availableGearDate?: Date) => - `${GEAR_SERVICE}/gear/filterGear?userProfilePk=${userProfilePk}${ - availableGearDate - ? `&${availableGearDate.getFullYear()}-${availableGearDate.getMonth()}-${availableGearDate.getDay()}` - : '' - }`; - -export const linkGear = (activityId: GCActivityId, gearUuid: string) => - `${GEAR_SERVICE}/gear/link/${gearUuid}/activity/${activityId}`; - -export const unlinkGear = (activityId: GCActivityId, gearUuid: string) => - `${GEAR_SERVICE}/gear/unlink/${gearUuid}/activity/${activityId}`; diff --git a/src/garmin/types.ts b/src/garmin/types.ts index 9897410..fb21097 100644 --- a/src/garmin/types.ts +++ b/src/garmin/types.ts @@ -3,6 +3,22 @@ export type GCUserHash = string; export type GCActivityId = number; export type GCWorkoutId = string; export type GCBadgeId = number; +export type GarminDomain = 'garmin.com' | 'garmin.cn'; + +export enum ExportFileType { + tcx = 'tcx', + gpx = 'gpx', + kml = 'kml', + zip = 'zip' +} + +export enum UploadFileType { + tcx = 'tcx', + gpx = 'gpx', + fit = 'fit' +} +export type ExportFileTypeValue = keyof typeof ExportFileType; +export type UploadFileTypeTypeValue = keyof typeof UploadFileType; export interface IUserInfo { userProfileId: GCUserProfileId; @@ -588,3 +604,249 @@ export interface Gear { createDate: string; updateDate: string; } + +export interface IOauth1Consumer { + key: string; + secret: string; +} +export interface IOauth1 { + token: IOauth1Token; + oauth: OAuth; +} + +export interface IGarminTokens { + oauth1: IOauth1Token; + oauth2: IOauth2Token; +} +export interface IOauth1Token { + oauth_token: string; + oauth_token_secret: string; +} + +export interface IOauth2Token { + // from Garmin API + scope: string; + jti: string; + access_token: string; + token_type: string; + refresh_token: string; + expires_in: number; + refresh_token_expires_in: number; + + // added + expires_at: number; + refresh_token_expires_at: number; + last_update_date: string; + expires_date: string; +} + +export interface IUserSettings { + id: number; + userData: IUserData; + userSleep: { + sleepTime: number; + defaultSleepTime: boolean; + wakeTime: number; + defaultWakeTime: boolean; + }; + connectDate: unknown; + sourceType: unknown; + userSleepWindows: IUserSleepWindows[]; +} +export interface IUserData { + gender: unknown; + weight: unknown; + height: unknown; + timeFormat: string; + birthDate: unknown; + measurementSystem: string; + activityLevel: unknown; + handedness: string; + powerFormat: { + formatId: number; + formatKey: string; + minFraction: number; + maxFraction: number; + groupingUsed: boolean; + displayFormat: unknown; + }; + heartRateFormat: { + formatId: number; + formatKey: string; + minFraction: number; + maxFraction: number; + groupingUsed: boolean; + displayFormat: unknown; + }; + firstDayOfWeek: { + dayId: number; + dayName: string; + sortOrder: number; + isPossibleFirstDay: boolean; + }; + vo2MaxRunning: unknown; + vo2MaxCycling: unknown; + lactateThresholdSpeed: unknown; + lactateThresholdHeartRate: unknown; + diveNumber: unknown; + intensityMinutesCalcMethod: string; + moderateIntensityMinutesHrZone: number; + vigorousIntensityMinutesHrZone: number; + hydrationMeasurementUnit: string; + hydrationContainers: unknown[]; + hydrationAutoGoalEnabled: boolean; + firstbeatMaxStressScore: unknown; + firstbeatCyclingLtTimestamp: unknown; + firstbeatRunningLtTimestamp: unknown; + thresholdHeartRateAutoDetected: unknown; + ftpAutoDetected: unknown; + trainingStatusPausedDate: unknown; + weatherLocation: { + useFixedLocation: unknown; + latitude: unknown; + longitude: unknown; + locationName: unknown; + isoCountryCode: unknown; + postalCode: unknown; + }; + golfDistanceUnit: string; + golfElevationUnit: unknown; + golfSpeedUnit: unknown; + externalBottomTime: unknown; +} +export interface IUserSleepWindows { + sleepWindowFrequency: string; + startSleepTimeSecondsFromMidnight: number; + endSleepTimeSecondsFromMidnight: number; +} + +export interface ICountActivities { + countOfActivities: number; + date: string; + stats: { + all: Record; + }; +} + +// Workouts + +export interface IWorkout { + workoutId?: number; + ownerId?: number; + workoutName: string; + description?: string; + updateDate: Date; + createdDate: Date; + sportType: ISportType; + trainingPlanId: null; + author: IAuthor; + estimatedDurationInSecs: number; + estimatedDistanceInMeters: null; + estimateType: null; + estimatedDistanceUnit: IUnit; + poolLength: number; + poolLengthUnit: IUnit; + workoutProvider: string; + workoutSourceId: string; + consumer: null; + atpPlanId: null; + workoutNameI18nKey: null; + descriptionI18nKey: null; + shared: boolean; + estimated: boolean; +} + +export interface IWorkoutDetail extends IWorkout { + workoutSegments: IWorkoutSegment[]; +} +export interface IAuthor { + userProfilePk: null; + displayName: null; + fullName: null; + profileImgNameLarge: null; + profileImgNameMedium: null; + profileImgNameSmall: null; + userPro: boolean; + vivokidUser: boolean; +} + +export interface IUnit { + unitId: null; + unitKey: null; + factor: null; +} + +export interface ISportType { + sportTypeId: number; + sportTypeKey: string; + displayOrder?: number; +} + +export interface IWorkoutSegment { + segmentOrder: number; + sportType: ISportType; + workoutSteps: IWorkoutStep[]; +} + +export interface IWorkoutStep { + type: string; + stepId: number; + stepOrder: number; + stepType: IStepType; + childStepId: null; + description: null; + endCondition: IEndCondition; + endConditionValue: number | null; + preferredEndConditionUnit: IUnit | null; + endConditionCompare: null; + targetType: ITargetType; + targetValueOne: null; + targetValueTwo: null; + targetValueUnit: null; + zoneNumber: null; + secondaryTargetType: null; + secondaryTargetValueOne: null; + secondaryTargetValueTwo: null; + secondaryTargetValueUnit: null; + secondaryZoneNumber: null; + endConditionZone: null; + strokeType: IStrokeType; + equipmentType: IEquipmentType; + category: null; + exerciseName: null; + workoutProvider: null; + providerExerciseSourceId: null; + weightValue: null; + weightUnit: null; +} + +export interface IEndCondition { + conditionTypeId: number; + conditionTypeKey: string; + displayOrder: number; + displayable: boolean; +} + +export interface IEquipmentType { + equipmentTypeId: number; + equipmentTypeKey: null; + displayOrder: number; +} + +export interface IStepType { + stepTypeId: number; + stepTypeKey: string; + displayOrder: number; +} + +export interface IStrokeType { + strokeTypeId: number; + strokeTypeKey: null; + displayOrder: number; +} + +export interface ITargetType { + workoutTargetTypeId: number; + workoutTargetTypeKey: string; + displayOrder: number; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..6597fcc --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs'; + +export const checkIsDirectory = (filePath: string): boolean => { + return fs.existsSync(filePath) && fs.lstatSync(filePath).isDirectory(); +}; + +export const createDirectory = (directoryPath: string): void => { + fs.mkdirSync(directoryPath); +}; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const writeToFile = (filePath: string, data: any): void => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + fs.writeFileSync(filePath, data, (error) => { + if (error) throw error; + }); +}; diff --git a/tsconfig.json b/tsconfig.json index bb24908..7839251 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,12 +2,12 @@ "compilerOptions": { "module": "CommonJS", "moduleResolution": "Node", - "target": "ES2018", + "target": "ES5", "sourceMap": true, "outDir": "dist", "allowJs": true, "esModuleInterop": true, - "lib": ["ES2018"], + "lib": ["ES5"], "resolveJsonModule": true, "noImplicitAny": true, "strictFunctionTypes": true,