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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # suunto-api-wrapper
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/suunto-api-wrapper.svg)](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
- git clone <this-repo> suunto-api-wrapper
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
- Then consume it from another project (e.g. via a local path or `npm link`):
47
-
48
- ```jsonc
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 = Buffer.from("Bh8nsTyCeC0Ql2drMen78awk84AE3ZxW");
216
- var RAW = Buffer.from(PART1 + PART2, "base64");
217
- var SHARED = Buffer.from(
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 generateXtotp(email, now = Date.now()) {
225
- const key = import_node_crypto.default.pbkdf2Sync(
226
- SHARED,
227
- Buffer.from(email, "utf8"),
228
- PBKDF2_ITERATIONS,
229
- PBKDF2_KEY_LENGTH,
230
- "sha1"
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 = Buffer.allocUnsafe(8);
234
- msg.writeBigUInt64BE(counter);
235
- const mac = import_node_crypto.default.createHmac("sha1", key).update(msg).digest();
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 binary = mac.readUInt32BE(offset) & 2147483647;
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 = Buffer.from("Bh8nsTyCeC0Ql2drMen78awk84AE3ZxW");
164
- var RAW = Buffer.from(PART1 + PART2, "base64");
165
- var SHARED = Buffer.from(
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 generateXtotp(email, now = Date.now()) {
173
- const key = crypto.pbkdf2Sync(
174
- SHARED,
175
- Buffer.from(email, "utf8"),
176
- PBKDF2_ITERATIONS,
177
- PBKDF2_KEY_LENGTH,
178
- "sha1"
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 = Buffer.allocUnsafe(8);
182
- msg.writeBigUInt64BE(counter);
183
- const mac = crypto.createHmac("sha1", key).update(msg).digest();
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 binary = mac.readUInt32BE(offset) & 2147483647;
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.0.0",
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",