suunto-api-wrapper 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -16
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +47 -30
- package/dist/index.mjs +47 -20
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# suunto-api-wrapper
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/suunto-api-wrapper)
|
|
4
|
+
|
|
3
5
|
A small, typed TypeScript client for the **Suunto app API** (which is served by
|
|
4
6
|
the Sports Tracker backend at `api.sports-tracker.com`).
|
|
5
7
|
|
|
@@ -34,25 +36,13 @@ If Suunto or Sports Tracker request it, this project will comply.
|
|
|
34
36
|
|
|
35
37
|
## Installation
|
|
36
38
|
|
|
37
|
-
This package isn't published to npm yet. Install it from source:
|
|
38
|
-
|
|
39
39
|
```bash
|
|
40
|
-
|
|
41
|
-
cd suunto-api-wrapper
|
|
42
|
-
npm install
|
|
43
|
-
npm run build # emits ESM + CJS + .d.ts into dist/
|
|
40
|
+
npm install suunto-api-wrapper
|
|
44
41
|
```
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// package.json
|
|
50
|
-
{
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"suunto-api-wrapper": "file:../suunto-api-wrapper"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
43
|
+
Requires **Node.js 20+** or any modern browser — it relies on the global
|
|
44
|
+
`fetch` and Web Crypto APIs, so it runs in Node and the browser (e.g. Angular,
|
|
45
|
+
React) with no polyfills.
|
|
56
46
|
|
|
57
47
|
The package ships both **ESM** and **CommonJS** builds with full type
|
|
58
48
|
declarations, so `import` and `require` both work:
|
package/dist/index.d.mts
CHANGED
|
@@ -64,7 +64,7 @@ declare class HttpClient {
|
|
|
64
64
|
private buildUrl;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
declare function generateXtotp(email: string, now?: number): string
|
|
67
|
+
declare function generateXtotp(email: string, now?: number): Promise<string>;
|
|
68
68
|
declare function secondsUntilRollover(now?: number): number;
|
|
69
69
|
|
|
70
70
|
interface LoginOptions {
|
package/dist/index.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ declare class HttpClient {
|
|
|
64
64
|
private buildUrl;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
declare function generateXtotp(email: string, now?: number): string
|
|
67
|
+
declare function generateXtotp(email: string, now?: number): Promise<string>;
|
|
68
68
|
declare function secondsUntilRollover(now?: number): number;
|
|
69
69
|
|
|
70
70
|
interface LoginOptions {
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -209,32 +199,59 @@ function isAbortError(err) {
|
|
|
209
199
|
}
|
|
210
200
|
|
|
211
201
|
// src/otp/index.ts
|
|
212
|
-
var import_node_crypto = __toESM(require("crypto"));
|
|
213
202
|
var PART1 = "FBkubDYmN28bWVQLLTsWWhI+NAtILCNlPQc5Y";
|
|
214
203
|
var PART2 = "BgiMRYjKA99Jj4HHFIqLmomOFttBQchNzcZU0QrODcDWz4hekc1QGNTPlciNhEKGl5GPDkzFyVX";
|
|
215
|
-
var XOR_KEY =
|
|
216
|
-
var RAW =
|
|
217
|
-
var SHARED =
|
|
218
|
-
RAW.map((b, i) => b ^ XOR_KEY[i % XOR_KEY.length])
|
|
219
|
-
);
|
|
204
|
+
var XOR_KEY = new TextEncoder().encode("Bh8nsTyCeC0Ql2drMen78awk84AE3ZxW");
|
|
205
|
+
var RAW = base64ToBytes(PART1 + PART2);
|
|
206
|
+
var SHARED = RAW.map((b, i) => b ^ XOR_KEY[i % XOR_KEY.length]);
|
|
220
207
|
var PBKDF2_ITERATIONS = 100;
|
|
221
208
|
var PBKDF2_KEY_LENGTH = 32;
|
|
222
209
|
var TOTP_PERIOD_MS = 3e4;
|
|
223
210
|
var TOTP_DIGITS = 6;
|
|
224
|
-
function
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
211
|
+
function getCrypto() {
|
|
212
|
+
const c = globalThis.crypto;
|
|
213
|
+
if (!c?.subtle) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"Web Crypto API is unavailable. Use a browser or Node 20+ (or a global `crypto` polyfill)."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return c;
|
|
219
|
+
}
|
|
220
|
+
function base64ToBytes(b64) {
|
|
221
|
+
const binary = atob(b64);
|
|
222
|
+
const bytes = new Uint8Array(binary.length);
|
|
223
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
224
|
+
return bytes;
|
|
225
|
+
}
|
|
226
|
+
async function generateXtotp(email, now = Date.now()) {
|
|
227
|
+
const subtle = getCrypto().subtle;
|
|
228
|
+
const baseKey = await subtle.importKey("raw", SHARED, "PBKDF2", false, [
|
|
229
|
+
"deriveBits"
|
|
230
|
+
]);
|
|
231
|
+
const derived = await subtle.deriveBits(
|
|
232
|
+
{
|
|
233
|
+
name: "PBKDF2",
|
|
234
|
+
salt: new TextEncoder().encode(email),
|
|
235
|
+
iterations: PBKDF2_ITERATIONS,
|
|
236
|
+
hash: "SHA-1"
|
|
237
|
+
},
|
|
238
|
+
baseKey,
|
|
239
|
+
PBKDF2_KEY_LENGTH * 8
|
|
240
|
+
);
|
|
241
|
+
const hmacKey = await subtle.importKey(
|
|
242
|
+
"raw",
|
|
243
|
+
derived,
|
|
244
|
+
{ name: "HMAC", hash: "SHA-1" },
|
|
245
|
+
false,
|
|
246
|
+
["sign"]
|
|
231
247
|
);
|
|
232
248
|
const counter = BigInt(Math.floor(now / TOTP_PERIOD_MS));
|
|
233
|
-
const msg =
|
|
234
|
-
msg.
|
|
235
|
-
const mac =
|
|
249
|
+
const msg = new Uint8Array(8);
|
|
250
|
+
new DataView(msg.buffer).setBigUint64(0, counter, false);
|
|
251
|
+
const mac = new Uint8Array(await subtle.sign("HMAC", hmacKey, msg));
|
|
236
252
|
const offset = mac[mac.length - 1] & 15;
|
|
237
|
-
const
|
|
253
|
+
const view = new DataView(mac.buffer, mac.byteOffset, mac.byteLength);
|
|
254
|
+
const binary = view.getUint32(offset, false) & 2147483647;
|
|
238
255
|
const code = binary % 10 ** TOTP_DIGITS;
|
|
239
256
|
return code.toString().padStart(TOTP_DIGITS, "0");
|
|
240
257
|
}
|
|
@@ -262,7 +279,7 @@ async function login(options) {
|
|
|
262
279
|
body,
|
|
263
280
|
headers: {
|
|
264
281
|
"user-agent": userAgent,
|
|
265
|
-
"x-totp": generateXtotp(email),
|
|
282
|
+
"x-totp": await generateXtotp(email),
|
|
266
283
|
"content-type": "application/x-www-form-urlencoded"
|
|
267
284
|
}
|
|
268
285
|
});
|
|
@@ -368,8 +385,8 @@ var SuuntoClient = class _SuuntoClient {
|
|
|
368
385
|
baseUrl: baseUrl ?? SPORTS_TRACKER_API,
|
|
369
386
|
headers: { "user-agent": userAgent, ...headers },
|
|
370
387
|
...rest,
|
|
371
|
-
beforeRequest: (ctx) => {
|
|
372
|
-
if (email) ctx.headers["x-totp"] = generateXtotp(email);
|
|
388
|
+
beforeRequest: async (ctx) => {
|
|
389
|
+
if (email) ctx.headers["x-totp"] = await generateXtotp(email);
|
|
373
390
|
if (sessionKey) ctx.headers["sttauthorization"] = sessionKey;
|
|
374
391
|
}
|
|
375
392
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -157,32 +157,59 @@ function isAbortError(err) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// src/otp/index.ts
|
|
160
|
-
import crypto from "crypto";
|
|
161
160
|
var PART1 = "FBkubDYmN28bWVQLLTsWWhI+NAtILCNlPQc5Y";
|
|
162
161
|
var PART2 = "BgiMRYjKA99Jj4HHFIqLmomOFttBQchNzcZU0QrODcDWz4hekc1QGNTPlciNhEKGl5GPDkzFyVX";
|
|
163
|
-
var XOR_KEY =
|
|
164
|
-
var RAW =
|
|
165
|
-
var SHARED =
|
|
166
|
-
RAW.map((b, i) => b ^ XOR_KEY[i % XOR_KEY.length])
|
|
167
|
-
);
|
|
162
|
+
var XOR_KEY = new TextEncoder().encode("Bh8nsTyCeC0Ql2drMen78awk84AE3ZxW");
|
|
163
|
+
var RAW = base64ToBytes(PART1 + PART2);
|
|
164
|
+
var SHARED = RAW.map((b, i) => b ^ XOR_KEY[i % XOR_KEY.length]);
|
|
168
165
|
var PBKDF2_ITERATIONS = 100;
|
|
169
166
|
var PBKDF2_KEY_LENGTH = 32;
|
|
170
167
|
var TOTP_PERIOD_MS = 3e4;
|
|
171
168
|
var TOTP_DIGITS = 6;
|
|
172
|
-
function
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
169
|
+
function getCrypto() {
|
|
170
|
+
const c = globalThis.crypto;
|
|
171
|
+
if (!c?.subtle) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"Web Crypto API is unavailable. Use a browser or Node 20+ (or a global `crypto` polyfill)."
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return c;
|
|
177
|
+
}
|
|
178
|
+
function base64ToBytes(b64) {
|
|
179
|
+
const binary = atob(b64);
|
|
180
|
+
const bytes = new Uint8Array(binary.length);
|
|
181
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
182
|
+
return bytes;
|
|
183
|
+
}
|
|
184
|
+
async function generateXtotp(email, now = Date.now()) {
|
|
185
|
+
const subtle = getCrypto().subtle;
|
|
186
|
+
const baseKey = await subtle.importKey("raw", SHARED, "PBKDF2", false, [
|
|
187
|
+
"deriveBits"
|
|
188
|
+
]);
|
|
189
|
+
const derived = await subtle.deriveBits(
|
|
190
|
+
{
|
|
191
|
+
name: "PBKDF2",
|
|
192
|
+
salt: new TextEncoder().encode(email),
|
|
193
|
+
iterations: PBKDF2_ITERATIONS,
|
|
194
|
+
hash: "SHA-1"
|
|
195
|
+
},
|
|
196
|
+
baseKey,
|
|
197
|
+
PBKDF2_KEY_LENGTH * 8
|
|
198
|
+
);
|
|
199
|
+
const hmacKey = await subtle.importKey(
|
|
200
|
+
"raw",
|
|
201
|
+
derived,
|
|
202
|
+
{ name: "HMAC", hash: "SHA-1" },
|
|
203
|
+
false,
|
|
204
|
+
["sign"]
|
|
179
205
|
);
|
|
180
206
|
const counter = BigInt(Math.floor(now / TOTP_PERIOD_MS));
|
|
181
|
-
const msg =
|
|
182
|
-
msg.
|
|
183
|
-
const mac =
|
|
207
|
+
const msg = new Uint8Array(8);
|
|
208
|
+
new DataView(msg.buffer).setBigUint64(0, counter, false);
|
|
209
|
+
const mac = new Uint8Array(await subtle.sign("HMAC", hmacKey, msg));
|
|
184
210
|
const offset = mac[mac.length - 1] & 15;
|
|
185
|
-
const
|
|
211
|
+
const view = new DataView(mac.buffer, mac.byteOffset, mac.byteLength);
|
|
212
|
+
const binary = view.getUint32(offset, false) & 2147483647;
|
|
186
213
|
const code = binary % 10 ** TOTP_DIGITS;
|
|
187
214
|
return code.toString().padStart(TOTP_DIGITS, "0");
|
|
188
215
|
}
|
|
@@ -210,7 +237,7 @@ async function login(options) {
|
|
|
210
237
|
body,
|
|
211
238
|
headers: {
|
|
212
239
|
"user-agent": userAgent,
|
|
213
|
-
"x-totp": generateXtotp(email),
|
|
240
|
+
"x-totp": await generateXtotp(email),
|
|
214
241
|
"content-type": "application/x-www-form-urlencoded"
|
|
215
242
|
}
|
|
216
243
|
});
|
|
@@ -316,8 +343,8 @@ var SuuntoClient = class _SuuntoClient {
|
|
|
316
343
|
baseUrl: baseUrl ?? SPORTS_TRACKER_API,
|
|
317
344
|
headers: { "user-agent": userAgent, ...headers },
|
|
318
345
|
...rest,
|
|
319
|
-
beforeRequest: (ctx) => {
|
|
320
|
-
if (email) ctx.headers["x-totp"] = generateXtotp(email);
|
|
346
|
+
beforeRequest: async (ctx) => {
|
|
347
|
+
if (email) ctx.headers["x-totp"] = await generateXtotp(email);
|
|
321
348
|
if (sessionKey) ctx.headers["sttauthorization"] = sessionKey;
|
|
322
349
|
}
|
|
323
350
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suunto-api-wrapper",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Unofficial typed TypeScript client for the Suunto app API (Sports Tracker backend). Not affiliated with or endorsed by Suunto or Sports Tracker.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/Marius-Ar/suunto-api-wrapper"
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
21
24
|
"scripts": {
|
|
22
25
|
"build": "tsup",
|
|
23
26
|
"dev": "tsup --watch",
|