@dlwiest/ts-tonal-client 0.0.1 → 0.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 +404 -38
- package/dist/examples/create-workout.d.ts +1 -0
- package/dist/examples/debug-daily-lifts.d.ts +1 -0
- package/dist/examples/delete-workout.d.ts +1 -0
- package/dist/examples/edit-workout.d.ts +1 -0
- package/dist/examples/estimate-workout.d.ts +1 -0
- package/dist/examples/get-achievement-stats.d.ts +2 -0
- package/dist/examples/get-achievements.d.ts +2 -0
- package/dist/examples/get-activity-summaries.d.ts +2 -0
- package/dist/examples/get-current-streak.d.ts +1 -0
- package/dist/examples/get-daily-lifts.d.ts +1 -0
- package/dist/examples/get-daily-metrics.d.ts +1 -0
- package/dist/examples/get-goal-metrics.d.ts +1 -0
- package/dist/examples/get-goals.d.ts +1 -0
- package/dist/examples/get-home-calendar.d.ts +2 -0
- package/dist/examples/get-metric-scores.d.ts +1 -0
- package/dist/examples/get-movements.d.ts +1 -0
- package/dist/examples/get-muscle-readiness.d.ts +1 -0
- package/dist/examples/get-program-by-id.d.ts +1 -0
- package/dist/examples/get-target-scores.d.ts +1 -0
- package/dist/examples/get-training-effect-goals.d.ts +1 -0
- package/dist/examples/get-training-types.d.ts +1 -0
- package/dist/examples/get-user-info.d.ts +1 -0
- package/dist/examples/get-user-settings.d.ts +1 -0
- package/dist/examples/get-user-statistics.d.ts +2 -0
- package/dist/examples/get-user-workouts.d.ts +1 -0
- package/dist/examples/get-workout-by-id.d.ts +1 -0
- package/dist/examples/get-workout-by-share-url.d.ts +1 -0
- package/dist/examples/raw-daily-lifts-test.d.ts +1 -0
- package/dist/index.d.ts +573 -29
- package/dist/index.esm.js +447 -119
- package/dist/index.js +451 -119
- package/dist/src/auth/auth-manager.d.ts +12 -0
- package/dist/src/client.d.ts +39 -0
- package/dist/src/http/http-client.d.ts +12 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/services/movement-service.d.ts +7 -0
- package/dist/src/services/user-service.d.ts +51 -0
- package/dist/src/services/workout-service.d.ts +14 -0
- package/dist/src/types/auth.d.ts +18 -0
- package/dist/src/types/common.d.ts +1 -0
- package/dist/src/types/index.d.ts +6 -0
- package/dist/src/types/movements.d.ts +30 -0
- package/dist/src/types/programs.d.ts +46 -0
- package/dist/src/types/users.d.ts +418 -0
- package/dist/src/types/workouts.d.ts +120 -0
- package/dist/src/types.d.ts +125 -0
- package/package.json +43 -4
package/dist/index.js
CHANGED
|
@@ -1,149 +1,481 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Copyright (c) Microsoft Corporation.
|
|
5
|
-
|
|
6
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
7
|
-
purpose with or without fee is hereby granted.
|
|
8
|
-
|
|
9
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
-
***************************************************************************** */
|
|
17
|
-
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
21
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
22
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
23
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
24
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
25
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
26
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
31
|
-
var e = new Error(message);
|
|
32
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
33
|
-
};
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
34
4
|
|
|
35
|
-
class
|
|
36
|
-
constructor(
|
|
37
|
-
|
|
38
|
-
this.
|
|
5
|
+
class TonalClientError extends Error {
|
|
6
|
+
constructor(message, statusCode, originalError) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'TonalClientError';
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.originalError = originalError;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class AuthManager {
|
|
15
|
+
constructor(username, password) {
|
|
39
16
|
this.idToken = '';
|
|
40
17
|
this.tokenExpiresAt = 0;
|
|
18
|
+
this.authUrl = 'https://tonal.auth0.com/oauth/token';
|
|
19
|
+
this.clientId = 'ERCyexW-xoVG_Yy3RDe-eV4xsOnRHP6L';
|
|
20
|
+
this.username = username;
|
|
21
|
+
this.password = password;
|
|
41
22
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
-
const data = {
|
|
23
|
+
async authenticate() {
|
|
24
|
+
if (this.isTokenValid()) {
|
|
25
|
+
return this.idToken;
|
|
26
|
+
}
|
|
27
|
+
const response = await fetch(this.authUrl, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({
|
|
54
33
|
username: this.username,
|
|
55
34
|
password: this.password,
|
|
56
|
-
client_id:
|
|
35
|
+
client_id: this.clientId,
|
|
57
36
|
grant_type: 'password',
|
|
58
37
|
scope: 'offline_access',
|
|
59
|
-
}
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const errorText = await response.text();
|
|
42
|
+
let errorData;
|
|
60
43
|
try {
|
|
61
|
-
|
|
62
|
-
method: 'POST',
|
|
63
|
-
headers: {
|
|
64
|
-
'Content-Type': 'application/json',
|
|
65
|
-
},
|
|
66
|
-
body: JSON.stringify(data),
|
|
67
|
-
});
|
|
68
|
-
if (response.status === 403) {
|
|
69
|
-
throw new Error('Invalid username or password');
|
|
70
|
-
}
|
|
71
|
-
const json = (yield response.json());
|
|
72
|
-
this.idToken = json.id_token;
|
|
73
|
-
this.tokenExpiresAt = Date.now() + json.expires_in * 1000;
|
|
44
|
+
errorData = JSON.parse(errorText);
|
|
74
45
|
}
|
|
75
|
-
catch
|
|
76
|
-
|
|
77
|
-
throw new Error('Failed to retrieve access token');
|
|
46
|
+
catch {
|
|
47
|
+
errorData = { error: errorText };
|
|
78
48
|
}
|
|
79
|
-
|
|
49
|
+
throw new TonalClientError(errorData.error_description || errorData.error || 'Authentication failed', response.status, errorData);
|
|
50
|
+
}
|
|
51
|
+
const tokenData = await response.json();
|
|
52
|
+
this.idToken = tokenData.id_token;
|
|
53
|
+
this.tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000);
|
|
54
|
+
return this.idToken;
|
|
80
55
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
56
|
+
getValidToken() {
|
|
57
|
+
if (!this.isTokenValid()) {
|
|
58
|
+
throw new TonalClientError('Token expired. Call authenticate() first.');
|
|
59
|
+
}
|
|
60
|
+
return this.idToken;
|
|
61
|
+
}
|
|
62
|
+
isTokenValid() {
|
|
63
|
+
return !!this.idToken && Date.now() < this.tokenExpiresAt - 60000; // 1 minute buffer
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class HttpClient {
|
|
68
|
+
constructor(authManager) {
|
|
69
|
+
this.authManager = authManager;
|
|
70
|
+
this.baseUrl = 'https://api.tonal.com/v6';
|
|
71
|
+
this.requestTimeout = 30000;
|
|
72
|
+
this.maxRetries = 3;
|
|
73
|
+
}
|
|
74
|
+
async request(endpoint, options = {}, expectsBody = true) {
|
|
75
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
76
|
+
return this.makeRequestWithRetry(url, options, expectsBody);
|
|
77
|
+
}
|
|
78
|
+
async makeRequest(url, options = {}, expectsBody = true) {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
|
|
81
|
+
try {
|
|
82
|
+
const token = this.authManager.getValidToken();
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
...options,
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${token}`,
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
...options.headers,
|
|
89
|
+
},
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
clearTimeout(timeoutId);
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errorText = await response.text();
|
|
95
|
+
let errorData;
|
|
96
|
+
try {
|
|
97
|
+
errorData = JSON.parse(errorText);
|
|
87
98
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
return (yield response.json());
|
|
99
|
+
catch {
|
|
100
|
+
errorData = { error: errorText };
|
|
101
|
+
}
|
|
102
|
+
throw new TonalClientError(errorData.error_description || errorData.error || `HTTP ${response.status}`, response.status, errorData);
|
|
94
103
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
throw new Error('Failed to retrieve movements');
|
|
104
|
+
if (expectsBody) {
|
|
105
|
+
return await response.json();
|
|
98
106
|
}
|
|
99
|
-
|
|
107
|
+
else {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
clearTimeout(timeoutId);
|
|
113
|
+
if (error instanceof TonalClientError) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
117
|
+
throw new TonalClientError('Request timeout', undefined, error);
|
|
118
|
+
}
|
|
119
|
+
throw new TonalClientError('Request failed', undefined, error);
|
|
120
|
+
}
|
|
100
121
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
async makeRequestWithRetry(url, options = {}, expectsBody = true) {
|
|
123
|
+
let lastError;
|
|
124
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
104
125
|
try {
|
|
105
|
-
|
|
106
|
-
yield this.refreshToken();
|
|
107
|
-
}
|
|
108
|
-
const response = yield fetch(`https://api.tonal.com/v6/workouts/${id}`, {
|
|
109
|
-
headers: {
|
|
110
|
-
Authorization: `Bearer ${this.idToken}`,
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
const json = (yield response.json());
|
|
114
|
-
return json;
|
|
126
|
+
return await this.makeRequest(url, options, expectsBody);
|
|
115
127
|
}
|
|
116
|
-
catch (
|
|
117
|
-
|
|
118
|
-
|
|
128
|
+
catch (error) {
|
|
129
|
+
lastError = error instanceof TonalClientError ? error : new TonalClientError('Unknown error', undefined, error);
|
|
130
|
+
if (attempt === this.maxRetries || (lastError.statusCode && lastError.statusCode < 500)) {
|
|
131
|
+
throw lastError;
|
|
132
|
+
}
|
|
133
|
+
const delay = Math.pow(2, attempt - 1) * 1000;
|
|
134
|
+
await this.sleep(delay);
|
|
119
135
|
}
|
|
136
|
+
}
|
|
137
|
+
throw lastError;
|
|
138
|
+
}
|
|
139
|
+
async sleep(ms) {
|
|
140
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
class WorkoutService {
|
|
145
|
+
constructor(httpClient) {
|
|
146
|
+
this.httpClient = httpClient;
|
|
147
|
+
}
|
|
148
|
+
async getUserWorkouts(offset = 0, limit = 50) {
|
|
149
|
+
return this.httpClient.request('/user-workouts', {
|
|
150
|
+
method: 'GET',
|
|
151
|
+
headers: {
|
|
152
|
+
'x-paginate-offset': offset.toString(),
|
|
153
|
+
'x-paginate-limit': limit.toString(),
|
|
154
|
+
},
|
|
120
155
|
});
|
|
121
156
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
157
|
+
async getDailyLifts(userInfo, timeZone) {
|
|
158
|
+
const device = userInfo.recentMobileDevice;
|
|
159
|
+
const userAgent = device.platform === 'ios'
|
|
160
|
+
? `Tonal/3004226 CFNetwork/3860.100.1 Darwin/${device.osVersion}`
|
|
161
|
+
: `Tonal/${device.appVersion}`;
|
|
162
|
+
// Use provided timezone, or auto-detect from system, or fall back to UTC
|
|
163
|
+
let detectedTimeZone = timeZone;
|
|
164
|
+
if (!detectedTimeZone) {
|
|
129
165
|
try {
|
|
130
|
-
|
|
131
|
-
yield this.refreshToken();
|
|
132
|
-
}
|
|
133
|
-
const response = yield fetch(`https://api.tonal.com/v6/user-workouts/sharing-records/${shareId}`, {
|
|
134
|
-
headers: {
|
|
135
|
-
Authorization: `Bearer ${this.idToken}`,
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
const json = (yield response.json());
|
|
139
|
-
return json;
|
|
166
|
+
detectedTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
140
167
|
}
|
|
141
|
-
catch
|
|
142
|
-
|
|
143
|
-
throw new Error('Failed to retrieve workout');
|
|
168
|
+
catch {
|
|
169
|
+
detectedTimeZone = 'UTC';
|
|
144
170
|
}
|
|
171
|
+
}
|
|
172
|
+
return this.httpClient.request(`/user-workouts?types=DailyLift`, {
|
|
173
|
+
method: 'GET',
|
|
174
|
+
headers: {
|
|
175
|
+
'Time-Zone': detectedTimeZone,
|
|
176
|
+
'AppVersion': device.appVersion,
|
|
177
|
+
'DeviceId': device.tonalDeviceId,
|
|
178
|
+
'Accept': '*/*',
|
|
179
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
180
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
181
|
+
'User-Agent': userAgent,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async getWorkoutById(workoutId) {
|
|
186
|
+
if (!workoutId?.trim()) {
|
|
187
|
+
throw new TonalClientError('Workout ID is required');
|
|
188
|
+
}
|
|
189
|
+
return this.httpClient.request(`/workouts/${workoutId}`);
|
|
190
|
+
}
|
|
191
|
+
async getWorkoutByShareUrl(shareUrl) {
|
|
192
|
+
if (!shareUrl?.trim()) {
|
|
193
|
+
throw new TonalClientError('Share URL is required');
|
|
194
|
+
}
|
|
195
|
+
const urlPattern = /https:\/\/share\.tonal\.com\/workout\/([a-f0-9-]+)/;
|
|
196
|
+
const match = shareUrl.match(urlPattern);
|
|
197
|
+
if (!match) {
|
|
198
|
+
throw new TonalClientError('Invalid share URL format. Expected: https://share.tonal.com/workout/{id}');
|
|
199
|
+
}
|
|
200
|
+
const shareId = match[1];
|
|
201
|
+
return this.httpClient.request(`/user-workouts/sharing-records/${encodeURIComponent(shareId)}`);
|
|
202
|
+
}
|
|
203
|
+
async estimateWorkoutDuration(sets) {
|
|
204
|
+
if (!sets?.length) {
|
|
205
|
+
throw new TonalClientError('At least one set is required for estimation');
|
|
206
|
+
}
|
|
207
|
+
return this.httpClient.request('/user-workouts/estimate', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
body: JSON.stringify({ sets }),
|
|
145
210
|
});
|
|
146
211
|
}
|
|
212
|
+
async createWorkout(workoutData) {
|
|
213
|
+
if (!workoutData.title?.trim()) {
|
|
214
|
+
throw new TonalClientError('Workout title is required');
|
|
215
|
+
}
|
|
216
|
+
if (!workoutData.sets?.length) {
|
|
217
|
+
throw new TonalClientError('At least one set is required');
|
|
218
|
+
}
|
|
219
|
+
const requestBody = {
|
|
220
|
+
title: workoutData.title,
|
|
221
|
+
sets: workoutData.sets,
|
|
222
|
+
createdSource: workoutData.createdSource || 'WorkoutBuilder',
|
|
223
|
+
shortDescription: workoutData.shortDescription || '',
|
|
224
|
+
description: workoutData.description || '',
|
|
225
|
+
};
|
|
226
|
+
return this.httpClient.request('/user-workouts', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
body: JSON.stringify(requestBody),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async updateWorkout(workoutData) {
|
|
232
|
+
if (!workoutData.id) {
|
|
233
|
+
throw new TonalClientError('Workout ID is required for updates');
|
|
234
|
+
}
|
|
235
|
+
if (!workoutData.title?.trim()) {
|
|
236
|
+
throw new TonalClientError('Workout title is required');
|
|
237
|
+
}
|
|
238
|
+
if (!workoutData.sets?.length) {
|
|
239
|
+
throw new TonalClientError('At least one set is required');
|
|
240
|
+
}
|
|
241
|
+
const requestBody = {
|
|
242
|
+
id: workoutData.id,
|
|
243
|
+
title: workoutData.title,
|
|
244
|
+
description: workoutData.description || '',
|
|
245
|
+
coachId: workoutData.coachId || '00000000-0000-0000-0000-000000000000',
|
|
246
|
+
sets: workoutData.sets,
|
|
247
|
+
level: workoutData.level || '',
|
|
248
|
+
assetId: workoutData.assetId,
|
|
249
|
+
createdSource: workoutData.createdSource || 'WorkoutBuilder'
|
|
250
|
+
};
|
|
251
|
+
return this.httpClient.request(`/user-workouts/${workoutData.id}`, {
|
|
252
|
+
method: 'PUT',
|
|
253
|
+
body: JSON.stringify(requestBody),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async deleteWorkout(workoutId) {
|
|
257
|
+
if (!workoutId?.trim()) {
|
|
258
|
+
throw new TonalClientError('Workout ID is required');
|
|
259
|
+
}
|
|
260
|
+
await this.httpClient.request(`/user-workouts/${workoutId}`, {
|
|
261
|
+
method: 'DELETE',
|
|
262
|
+
}, false);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class MovementService {
|
|
267
|
+
constructor(httpClient) {
|
|
268
|
+
this.httpClient = httpClient;
|
|
269
|
+
}
|
|
270
|
+
async getMovements() {
|
|
271
|
+
return this.httpClient.request('/movements');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
class UserService {
|
|
276
|
+
constructor(httpClient) {
|
|
277
|
+
this.httpClient = httpClient;
|
|
278
|
+
}
|
|
279
|
+
async getUserInfo() {
|
|
280
|
+
return this.httpClient.request('/users/userinfo');
|
|
281
|
+
}
|
|
282
|
+
async getGoals() {
|
|
283
|
+
return this.httpClient.request('/goals');
|
|
284
|
+
}
|
|
285
|
+
async getTrainingEffectGoals() {
|
|
286
|
+
return this.httpClient.request('/training-effect-goals');
|
|
287
|
+
}
|
|
288
|
+
async getTrainingTypes() {
|
|
289
|
+
return this.httpClient.request('/training-types');
|
|
290
|
+
}
|
|
291
|
+
async getGoalMetrics() {
|
|
292
|
+
return this.httpClient.request('/goal-metrics');
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Register or update a personal device for the user.
|
|
296
|
+
*
|
|
297
|
+
* NOTE: This endpoint has side effects and should be used with caution.
|
|
298
|
+
* It registers the device with Tonal's system and may affect notification
|
|
299
|
+
* settings and device management. For reverse-engineered clients, this
|
|
300
|
+
* could be considered unauthorized device registration.
|
|
301
|
+
*
|
|
302
|
+
* @internal This method is primarily for API completeness and documentation
|
|
303
|
+
*/
|
|
304
|
+
async registerPersonalDevice(userId, deviceInfo) {
|
|
305
|
+
return this.httpClient.request(`/users/${userId}/personal-devices`, {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
body: JSON.stringify(deviceInfo),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get user privacy permissions/settings.
|
|
312
|
+
*
|
|
313
|
+
* Returns what information this user has made public vs private.
|
|
314
|
+
* Useful for understanding social features and respecting privacy preferences.
|
|
315
|
+
*
|
|
316
|
+
* @internal This method is for understanding user privacy settings
|
|
317
|
+
*/
|
|
318
|
+
async getUserPermissions(userId) {
|
|
319
|
+
return this.httpClient.request(`/users/${userId}/permissions`);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get comprehensive user settings and preferences.
|
|
323
|
+
*
|
|
324
|
+
* Returns audio/visual settings, feature flags, onboarding states, and user preferences.
|
|
325
|
+
* Provides insight into Tonal's full feature ecosystem and user customization options.
|
|
326
|
+
*
|
|
327
|
+
* @internal This method is for understanding user preferences and feature usage
|
|
328
|
+
*/
|
|
329
|
+
async getUserSettings(userId) {
|
|
330
|
+
return this.httpClient.request(`/users/${userId}/user-settings`);
|
|
331
|
+
}
|
|
332
|
+
async getDailyMetrics(userId, days = 60) {
|
|
333
|
+
return this.httpClient.request(`/users/${userId}/metrics/daily?days=${days}`);
|
|
334
|
+
}
|
|
335
|
+
async getCurrentStreak(userId) {
|
|
336
|
+
return this.httpClient.request(`/users/${userId}/streaks/current`);
|
|
337
|
+
}
|
|
338
|
+
async getActivitySummaries(userId) {
|
|
339
|
+
return this.httpClient.request(`/users/${userId}/activity-summaries`);
|
|
340
|
+
}
|
|
341
|
+
async getUserStatistics(userId) {
|
|
342
|
+
return this.httpClient.request(`/users/${userId}/statistics`);
|
|
343
|
+
}
|
|
344
|
+
async getAchievementStats(userId) {
|
|
345
|
+
return this.httpClient.request(`/users/${userId}/achievement-stats`);
|
|
346
|
+
}
|
|
347
|
+
async getAchievements(userId) {
|
|
348
|
+
return this.httpClient.request(`/users/${userId}/achievements`);
|
|
349
|
+
}
|
|
350
|
+
async getHomeCalendar(userId) {
|
|
351
|
+
return this.httpClient.request(`/users/${userId}/calendar/home`);
|
|
352
|
+
}
|
|
353
|
+
async getMuscleReadiness(userId) {
|
|
354
|
+
return this.httpClient.request(`/users/${userId}/muscle-readiness/current`);
|
|
355
|
+
}
|
|
356
|
+
async getProgramById(programId) {
|
|
357
|
+
return this.httpClient.request(`/programs/${programId}`);
|
|
358
|
+
}
|
|
359
|
+
async getTargetScores(userId) {
|
|
360
|
+
return this.httpClient.request(`/users/${userId}/target-scores`);
|
|
361
|
+
}
|
|
362
|
+
async getMetricScores(userId, startWeek) {
|
|
363
|
+
const url = startWeek
|
|
364
|
+
? `/users/${userId}/metric-scores?startWeek=${startWeek}`
|
|
365
|
+
: `/users/${userId}/metric-scores`;
|
|
366
|
+
return this.httpClient.request(url);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
class TonalClient {
|
|
371
|
+
constructor(username, password) {
|
|
372
|
+
this.authManager = new AuthManager(username, password);
|
|
373
|
+
this.httpClient = new HttpClient(this.authManager);
|
|
374
|
+
this.workoutService = new WorkoutService(this.httpClient);
|
|
375
|
+
this.movementService = new MovementService(this.httpClient);
|
|
376
|
+
this.userService = new UserService(this.httpClient);
|
|
377
|
+
}
|
|
378
|
+
static async create(credentials) {
|
|
379
|
+
const client = new TonalClient(credentials.username, credentials.password);
|
|
380
|
+
await client.authManager.authenticate();
|
|
381
|
+
return client;
|
|
382
|
+
}
|
|
383
|
+
// Movement operations
|
|
384
|
+
async getMovements() {
|
|
385
|
+
return this.movementService.getMovements();
|
|
386
|
+
}
|
|
387
|
+
// User operations
|
|
388
|
+
async getUserInfo() {
|
|
389
|
+
return this.userService.getUserInfo();
|
|
390
|
+
}
|
|
391
|
+
async getGoals() {
|
|
392
|
+
return this.userService.getGoals();
|
|
393
|
+
}
|
|
394
|
+
async getTrainingEffectGoals() {
|
|
395
|
+
return this.userService.getTrainingEffectGoals();
|
|
396
|
+
}
|
|
397
|
+
async getTrainingTypes() {
|
|
398
|
+
return this.userService.getTrainingTypes();
|
|
399
|
+
}
|
|
400
|
+
async getGoalMetrics() {
|
|
401
|
+
return this.userService.getGoalMetrics();
|
|
402
|
+
}
|
|
403
|
+
async getUserSettings() {
|
|
404
|
+
const userInfo = await this.getUserInfo();
|
|
405
|
+
return this.userService.getUserSettings(userInfo.id);
|
|
406
|
+
}
|
|
407
|
+
async getDailyMetrics(days = 60) {
|
|
408
|
+
const userInfo = await this.getUserInfo();
|
|
409
|
+
return this.userService.getDailyMetrics(userInfo.id, days);
|
|
410
|
+
}
|
|
411
|
+
async getCurrentStreak() {
|
|
412
|
+
const userInfo = await this.getUserInfo();
|
|
413
|
+
return this.userService.getCurrentStreak(userInfo.id);
|
|
414
|
+
}
|
|
415
|
+
async getActivitySummaries() {
|
|
416
|
+
const userInfo = await this.getUserInfo();
|
|
417
|
+
return this.userService.getActivitySummaries(userInfo.id);
|
|
418
|
+
}
|
|
419
|
+
async getUserStatistics() {
|
|
420
|
+
const userInfo = await this.getUserInfo();
|
|
421
|
+
return this.userService.getUserStatistics(userInfo.id);
|
|
422
|
+
}
|
|
423
|
+
async getAchievementStats() {
|
|
424
|
+
const userInfo = await this.getUserInfo();
|
|
425
|
+
return this.userService.getAchievementStats(userInfo.id);
|
|
426
|
+
}
|
|
427
|
+
async getAchievements() {
|
|
428
|
+
const userInfo = await this.getUserInfo();
|
|
429
|
+
return this.userService.getAchievements(userInfo.id);
|
|
430
|
+
}
|
|
431
|
+
async getHomeCalendar() {
|
|
432
|
+
const userInfo = await this.getUserInfo();
|
|
433
|
+
return this.userService.getHomeCalendar(userInfo.id);
|
|
434
|
+
}
|
|
435
|
+
async getMuscleReadiness() {
|
|
436
|
+
const userInfo = await this.getUserInfo();
|
|
437
|
+
return this.userService.getMuscleReadiness(userInfo.id);
|
|
438
|
+
}
|
|
439
|
+
async getProgramById(programId) {
|
|
440
|
+
return this.userService.getProgramById(programId);
|
|
441
|
+
}
|
|
442
|
+
async getTargetScores() {
|
|
443
|
+
const userInfo = await this.getUserInfo();
|
|
444
|
+
return this.userService.getTargetScores(userInfo.id);
|
|
445
|
+
}
|
|
446
|
+
async getMetricScores(startWeek) {
|
|
447
|
+
const userInfo = await this.getUserInfo();
|
|
448
|
+
return this.userService.getMetricScores(userInfo.id, startWeek);
|
|
449
|
+
}
|
|
450
|
+
// Workout operations
|
|
451
|
+
async getUserWorkouts(offset = 0, limit = 50) {
|
|
452
|
+
return this.workoutService.getUserWorkouts(offset, limit);
|
|
453
|
+
}
|
|
454
|
+
async getDailyLifts(timeZone) {
|
|
455
|
+
// Get user info to populate device-specific headers
|
|
456
|
+
const userInfo = await this.getUserInfo();
|
|
457
|
+
return this.workoutService.getDailyLifts(userInfo, timeZone);
|
|
458
|
+
}
|
|
459
|
+
async getWorkoutById(workoutId) {
|
|
460
|
+
return this.workoutService.getWorkoutById(workoutId);
|
|
461
|
+
}
|
|
462
|
+
async getWorkoutByShareUrl(shareUrl) {
|
|
463
|
+
return this.workoutService.getWorkoutByShareUrl(shareUrl);
|
|
464
|
+
}
|
|
465
|
+
async estimateWorkoutDuration(sets) {
|
|
466
|
+
return this.workoutService.estimateWorkoutDuration(sets);
|
|
467
|
+
}
|
|
468
|
+
async createWorkout(workoutData) {
|
|
469
|
+
return this.workoutService.createWorkout(workoutData);
|
|
470
|
+
}
|
|
471
|
+
async updateWorkout(workoutData) {
|
|
472
|
+
return this.workoutService.updateWorkout(workoutData);
|
|
473
|
+
}
|
|
474
|
+
async deleteWorkout(workoutId) {
|
|
475
|
+
return this.workoutService.deleteWorkout(workoutId);
|
|
476
|
+
}
|
|
147
477
|
}
|
|
148
478
|
|
|
149
|
-
|
|
479
|
+
exports.TonalClient = TonalClient;
|
|
480
|
+
exports.TonalClientError = TonalClientError;
|
|
481
|
+
exports.default = TonalClient;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class AuthManager {
|
|
2
|
+
private username;
|
|
3
|
+
private password;
|
|
4
|
+
private idToken;
|
|
5
|
+
private tokenExpiresAt;
|
|
6
|
+
private readonly authUrl;
|
|
7
|
+
private readonly clientId;
|
|
8
|
+
constructor(username: string, password: string);
|
|
9
|
+
authenticate(): Promise<string>;
|
|
10
|
+
getValidToken(): string;
|
|
11
|
+
private isTokenValid;
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TonalMovement, TonalSharedWorkout, TonalWorkout, TonalUserInfo, TonalGoal, TonalTrainingEffectGoalsResponse, TonalTrainingType, TonalGoalMetric, TonalUserSettings, TonalDailyMetrics, TonalCurrentStreak, TonalActivitySummary, TonalUserStatistics, TonalAchievementStats, TonalEarnedAchievement, TonalHomeCalendar, TonalWorkoutEstimateSet, TonalWorkoutEstimateResponse, TonalWorkoutCreateRequest, TonalWorkoutUpdateRequest, TonalMuscleReadiness, TonalProgram, TonalTargetScoresResponse, TonalMetricScoresResponse } from './types';
|
|
2
|
+
export declare class TonalClient {
|
|
3
|
+
private authManager;
|
|
4
|
+
private httpClient;
|
|
5
|
+
private workoutService;
|
|
6
|
+
private movementService;
|
|
7
|
+
private userService;
|
|
8
|
+
private constructor();
|
|
9
|
+
static create(credentials: {
|
|
10
|
+
username: string;
|
|
11
|
+
password: string;
|
|
12
|
+
}): Promise<TonalClient>;
|
|
13
|
+
getMovements(): Promise<TonalMovement[]>;
|
|
14
|
+
getUserInfo(): Promise<TonalUserInfo>;
|
|
15
|
+
getGoals(): Promise<TonalGoal[]>;
|
|
16
|
+
getTrainingEffectGoals(): Promise<TonalTrainingEffectGoalsResponse>;
|
|
17
|
+
getTrainingTypes(): Promise<TonalTrainingType[]>;
|
|
18
|
+
getGoalMetrics(): Promise<TonalGoalMetric[]>;
|
|
19
|
+
getUserSettings(): Promise<TonalUserSettings>;
|
|
20
|
+
getDailyMetrics(days?: number): Promise<TonalDailyMetrics[]>;
|
|
21
|
+
getCurrentStreak(): Promise<TonalCurrentStreak>;
|
|
22
|
+
getActivitySummaries(): Promise<TonalActivitySummary[]>;
|
|
23
|
+
getUserStatistics(): Promise<TonalUserStatistics>;
|
|
24
|
+
getAchievementStats(): Promise<TonalAchievementStats>;
|
|
25
|
+
getAchievements(): Promise<TonalEarnedAchievement[]>;
|
|
26
|
+
getHomeCalendar(): Promise<TonalHomeCalendar>;
|
|
27
|
+
getMuscleReadiness(): Promise<TonalMuscleReadiness>;
|
|
28
|
+
getProgramById(programId: string): Promise<TonalProgram>;
|
|
29
|
+
getTargetScores(): Promise<TonalTargetScoresResponse>;
|
|
30
|
+
getMetricScores(startWeek?: number): Promise<TonalMetricScoresResponse>;
|
|
31
|
+
getUserWorkouts(offset?: number, limit?: number): Promise<TonalWorkout[]>;
|
|
32
|
+
getDailyLifts(timeZone?: string): Promise<TonalWorkout[]>;
|
|
33
|
+
getWorkoutById(workoutId: string): Promise<TonalWorkout>;
|
|
34
|
+
getWorkoutByShareUrl(shareUrl: string): Promise<TonalSharedWorkout>;
|
|
35
|
+
estimateWorkoutDuration(sets: TonalWorkoutEstimateSet[]): Promise<TonalWorkoutEstimateResponse>;
|
|
36
|
+
createWorkout(workoutData: TonalWorkoutCreateRequest): Promise<TonalWorkout>;
|
|
37
|
+
updateWorkout(workoutData: TonalWorkoutUpdateRequest): Promise<TonalWorkout>;
|
|
38
|
+
deleteWorkout(workoutId: string): Promise<void>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AuthManager } from '../auth/auth-manager';
|
|
2
|
+
export declare class HttpClient {
|
|
3
|
+
private authManager;
|
|
4
|
+
private readonly baseUrl;
|
|
5
|
+
private readonly requestTimeout;
|
|
6
|
+
private readonly maxRetries;
|
|
7
|
+
constructor(authManager: AuthManager);
|
|
8
|
+
request<T>(endpoint: string, options?: RequestInit, expectsBody?: boolean): Promise<T>;
|
|
9
|
+
private makeRequest;
|
|
10
|
+
private makeRequestWithRetry;
|
|
11
|
+
private sleep;
|
|
12
|
+
}
|