@nativesquare/soma 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/client/index.d.ts +83 -0
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +131 -0
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/_generated/component.d.ts +159 -0
  6. package/dist/component/_generated/component.d.ts.map +1 -1
  7. package/dist/component/garmin.d.ts +190 -6
  8. package/dist/component/garmin.d.ts.map +1 -1
  9. package/dist/component/garmin.js +805 -25
  10. package/dist/component/garmin.js.map +1 -1
  11. package/dist/component/private.d.ts +18 -0
  12. package/dist/component/private.d.ts.map +1 -1
  13. package/dist/component/private.js +18 -0
  14. package/dist/component/private.js.map +1 -1
  15. package/dist/component/public.d.ts +88 -42
  16. package/dist/component/public.d.ts.map +1 -1
  17. package/dist/component/public.js +12 -2
  18. package/dist/component/public.js.map +1 -1
  19. package/dist/component/schema.d.ts +87 -32
  20. package/dist/component/schema.d.ts.map +1 -1
  21. package/dist/component/schema.js +2 -1
  22. package/dist/component/schema.js.map +1 -1
  23. package/dist/component/validators/connection.d.ts +1 -0
  24. package/dist/component/validators/connection.d.ts.map +1 -1
  25. package/dist/component/validators/connection.js +2 -0
  26. package/dist/component/validators/connection.js.map +1 -1
  27. package/dist/component/validators/daily.d.ts +40 -5
  28. package/dist/component/validators/daily.d.ts.map +1 -1
  29. package/dist/component/validators/daily.js +10 -1
  30. package/dist/component/validators/daily.js.map +1 -1
  31. package/dist/component/validators/enums.d.ts +1 -1
  32. package/dist/component/validators/plannedWorkout.d.ts +5 -1
  33. package/dist/component/validators/plannedWorkout.d.ts.map +1 -1
  34. package/dist/component/validators/plannedWorkout.js +4 -0
  35. package/dist/component/validators/plannedWorkout.js.map +1 -1
  36. package/dist/component/validators/sleep.d.ts +8 -8
  37. package/dist/garmin/activity.d.ts +7 -16
  38. package/dist/garmin/activity.d.ts.map +1 -1
  39. package/dist/garmin/activity.js +17 -23
  40. package/dist/garmin/activity.js.map +1 -1
  41. package/dist/garmin/bloodPressure.d.ts +28 -0
  42. package/dist/garmin/bloodPressure.d.ts.map +1 -0
  43. package/dist/garmin/bloodPressure.js +34 -0
  44. package/dist/garmin/bloodPressure.js.map +1 -0
  45. package/dist/garmin/body.js +1 -1
  46. package/dist/garmin/body.js.map +1 -1
  47. package/dist/garmin/client.d.ts +117 -17
  48. package/dist/garmin/client.d.ts.map +1 -1
  49. package/dist/garmin/client.js +337 -43
  50. package/dist/garmin/client.js.map +1 -1
  51. package/dist/garmin/daily.d.ts.map +1 -1
  52. package/dist/garmin/daily.js +3 -3
  53. package/dist/garmin/daily.js.map +1 -1
  54. package/dist/garmin/hrv.d.ts +30 -0
  55. package/dist/garmin/hrv.d.ts.map +1 -0
  56. package/dist/garmin/hrv.js +45 -0
  57. package/dist/garmin/hrv.js.map +1 -0
  58. package/dist/garmin/index.d.ts +16 -2
  59. package/dist/garmin/index.d.ts.map +1 -1
  60. package/dist/garmin/index.js +8 -1
  61. package/dist/garmin/index.js.map +1 -1
  62. package/dist/garmin/maps/activity-type.d.ts +1 -2
  63. package/dist/garmin/maps/activity-type.d.ts.map +1 -1
  64. package/dist/garmin/maps/activity-type.js +1 -0
  65. package/dist/garmin/maps/activity-type.js.map +1 -1
  66. package/dist/garmin/menstruation.d.ts +6 -4
  67. package/dist/garmin/menstruation.d.ts.map +1 -1
  68. package/dist/garmin/menstruation.js +12 -8
  69. package/dist/garmin/menstruation.js.map +1 -1
  70. package/dist/garmin/pulseOx.d.ts +24 -0
  71. package/dist/garmin/pulseOx.d.ts.map +1 -0
  72. package/dist/garmin/pulseOx.js +33 -0
  73. package/dist/garmin/pulseOx.js.map +1 -0
  74. package/dist/garmin/respiration.d.ts +29 -0
  75. package/dist/garmin/respiration.d.ts.map +1 -0
  76. package/dist/garmin/respiration.js +42 -0
  77. package/dist/garmin/respiration.js.map +1 -0
  78. package/dist/garmin/skinTemp.d.ts +27 -0
  79. package/dist/garmin/skinTemp.d.ts.map +1 -0
  80. package/dist/garmin/skinTemp.js +35 -0
  81. package/dist/garmin/skinTemp.js.map +1 -0
  82. package/dist/garmin/sleep.d.ts +4 -4
  83. package/dist/garmin/sleep.d.ts.map +1 -1
  84. package/dist/garmin/sleep.js +15 -9
  85. package/dist/garmin/sleep.js.map +1 -1
  86. package/dist/garmin/stressDetails.d.ts +30 -0
  87. package/dist/garmin/stressDetails.d.ts.map +1 -0
  88. package/dist/garmin/stressDetails.js +49 -0
  89. package/dist/garmin/stressDetails.js.map +1 -0
  90. package/dist/garmin/sync.d.ts +14 -0
  91. package/dist/garmin/sync.d.ts.map +1 -1
  92. package/dist/garmin/sync.js +287 -5
  93. package/dist/garmin/sync.js.map +1 -1
  94. package/dist/garmin/types.d.ts +77 -186
  95. package/dist/garmin/types.d.ts.map +1 -1
  96. package/dist/garmin/types.js +4 -2
  97. package/dist/garmin/types.js.map +1 -1
  98. package/dist/garmin/userMetrics.d.ts +23 -0
  99. package/dist/garmin/userMetrics.d.ts.map +1 -0
  100. package/dist/garmin/userMetrics.js +41 -0
  101. package/dist/garmin/userMetrics.js.map +1 -0
  102. package/dist/validators.d.ts +107 -28
  103. package/dist/validators.d.ts.map +1 -1
  104. package/package.json +133 -124
  105. package/src/client/index.ts +199 -0
  106. package/src/component/_generated/component.ts +161 -2
  107. package/src/component/garmin.ts +898 -26
  108. package/src/component/private.ts +21 -0
  109. package/src/component/public.ts +11 -2
  110. package/src/component/schema.ts +2 -1
  111. package/src/component/validators/connection.ts +2 -0
  112. package/src/component/validators/daily.ts +15 -0
  113. package/src/component/validators/plannedWorkout.ts +4 -0
  114. package/src/garmin/activity.test.ts +13 -21
  115. package/src/garmin/activity.ts +38 -45
  116. package/src/garmin/bloodPressure.ts +41 -0
  117. package/src/garmin/body.ts +1 -1
  118. package/src/garmin/client.ts +550 -71
  119. package/src/garmin/daily.ts +8 -4
  120. package/src/garmin/hrv.ts +57 -0
  121. package/src/garmin/index.ts +77 -7
  122. package/src/garmin/maps/activity-type.ts +2 -2
  123. package/src/garmin/menstruation.ts +14 -12
  124. package/src/garmin/pulseOx.ts +45 -0
  125. package/src/garmin/respiration.ts +55 -0
  126. package/src/garmin/skinTemp.ts +42 -0
  127. package/src/garmin/sleep.test.ts +5 -6
  128. package/src/garmin/sleep.ts +22 -16
  129. package/src/garmin/spec/wellness-api.json +1 -0
  130. package/src/garmin/stressDetails.ts +71 -0
  131. package/src/garmin/sync.ts +348 -5
  132. package/src/garmin/types.ts +88 -300
  133. package/src/garmin/userMetrics.ts +50 -0
  134. package/src/garmin/wellness-api.d.ts +5637 -0
@@ -1,13 +1,28 @@
1
1
  // ─── Garmin Health API Client ────────────────────────────────────────────────
2
- // Lightweight, fetch-based client for the Garmin Health API.
3
- // Authenticates requests with an OAuth 2.0 Bearer token.
2
+ // Uses openapi-fetch for type-safe Wellness API calls and manual fetch for
3
+ // Training API endpoints that are not covered by the OpenAPI spec.
4
4
 
5
+ import createClient from "openapi-fetch";
6
+ import type { Middleware } from "openapi-fetch";
7
+ import type { paths } from "./wellness-api.js";
5
8
  import type {
6
9
  GarminActivity,
7
- GarminDailySummary,
8
- GarminSleep,
10
+ GarminActivityDetail,
11
+ GarminDailyExtended,
12
+ GarminSleepExtended,
9
13
  GarminBodyComposition,
10
- GarminMenstrualCycleData,
14
+ GarminMenstrualCycle,
15
+ GarminUserMetrics,
16
+ GarminStressDetail,
17
+ GarminSkinTemperature,
18
+ GarminRespiration,
19
+ GarminPulseOx,
20
+ GarminMoveIQEvent,
21
+ GarminHRVSummary,
22
+ GarminHealthSnapshot,
23
+ GarminEpoch,
24
+ GarminBloodPressure,
25
+ GarminSolar,
11
26
  GarminWorkout,
12
27
  GarminWorkoutSchedule,
13
28
  } from "./types.js";
@@ -25,7 +40,10 @@ export interface GarminClientOptions {
25
40
  }
26
41
 
27
42
  /**
28
- * A lightweight client for the Garmin Health API.
43
+ * A client for the Garmin Health API.
44
+ *
45
+ * Wellness API endpoints use openapi-fetch for type safety. Training API
46
+ * endpoints use manual fetch since they are not part of the Wellness API spec.
29
47
  *
30
48
  * All requests are authenticated with a Bearer token. Time-range parameters
31
49
  * use Unix epoch seconds for `uploadStartTimeInSeconds` and
@@ -46,27 +64,89 @@ export interface GarminClientOptions {
46
64
  export class GarminClient {
47
65
  private readonly accessToken: string;
48
66
  private readonly baseUrl: string;
67
+ private readonly wellness;
49
68
 
50
69
  constructor(opts: GarminClientOptions) {
51
70
  this.accessToken = opts.accessToken;
52
71
  this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
72
+
73
+ const authMiddleware: Middleware = {
74
+ async onRequest({ request }) {
75
+ request.headers.set("Authorization", `Bearer ${opts.accessToken}`);
76
+ request.headers.set("Accept", "application/json");
77
+ return request;
78
+ },
79
+ };
80
+
81
+ this.wellness = createClient<paths>({
82
+ baseUrl: `${this.baseUrl}/wellness-api`,
83
+ });
84
+ this.wellness.use(authMiddleware);
85
+ }
86
+
87
+ // ─── User Identity ──────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Resolve the Garmin user ID for the authenticated user.
91
+ *
92
+ * Garmin API: `GET /wellness-api/rest/user/id`
93
+ */
94
+ async getUserId(): Promise<string | null> {
95
+ const { data, error, response } = await this.wellness.GET("/rest/user/id");
96
+ if (error) {
97
+ throw new GarminApiError(
98
+ `Garmin API error: ${response.status} ${response.statusText}`,
99
+ response.status,
100
+ JSON.stringify(error),
101
+ );
102
+ }
103
+ return data?.userId ?? null;
104
+ }
105
+
106
+ // ─── User Permissions ─────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Check which permissions the user has granted.
110
+ *
111
+ * Garmin API: `GET /wellness-api/rest/user/permissions`
112
+ */
113
+ async getUserPermissions(): Promise<string[]> {
114
+ const { data, error, response } = await this.wellness.GET(
115
+ "/rest/user/permissions",
116
+ );
117
+ if (error) {
118
+ throw new GarminApiError(
119
+ `Garmin API error: ${response.status} ${response.statusText}`,
120
+ response.status,
121
+ JSON.stringify(error),
122
+ );
123
+ }
124
+ return data?.permissions ?? [];
53
125
  }
54
126
 
55
- // ─── Daily Summaries ────────────────────────────────────────────────────
127
+ // ─── Daily Summaries ──────────────────────────────────────────────────
56
128
 
57
129
  /**
58
130
  * Get daily wellness summaries.
59
131
  *
60
132
  * Garmin API: `GET /wellness-api/rest/dailies`
61
133
  */
62
- async getDailies(params: TimeRangeParams): Promise<GarminDailySummary[]> {
63
- return this.get<GarminDailySummary[]>(
64
- "/wellness-api/rest/dailies",
65
- timeRangeQuery(params),
66
- );
134
+ async getDailies(params: TimeRangeParams): Promise<GarminDailyExtended[]> {
135
+ const { data, error, response } = await this.wellness.GET("/rest/dailies", {
136
+ params: { query: timeRangeQuery(params, this.accessToken) },
137
+ });
138
+ if (error) {
139
+ throw new GarminApiError(
140
+ `Garmin API error: ${response.status} ${response.statusText}`,
141
+ response.status,
142
+ JSON.stringify(error),
143
+ );
144
+ }
145
+ console.log("[getDailies] raw response:", JSON.stringify(data?.[0], null, 2));
146
+ return (data ?? []) as GarminDailyExtended[];
67
147
  }
68
148
 
69
- // ─── Activities ─────────────────────────────────────────────────────────
149
+ // ─── Activities ───────────────────────────────────────────────────────
70
150
 
71
151
  /**
72
152
  * Get activity summaries.
@@ -74,24 +154,97 @@ export class GarminClient {
74
154
  * Garmin API: `GET /wellness-api/rest/activities`
75
155
  */
76
156
  async getActivities(params: TimeRangeParams): Promise<GarminActivity[]> {
77
- return this.get<GarminActivity[]>(
78
- "/wellness-api/rest/activities",
79
- timeRangeQuery(params),
157
+ const { data, error, response } = await this.wellness.GET(
158
+ "/rest/activities",
159
+ {
160
+ params: { query: timeRangeQuery(params, this.accessToken) },
161
+ },
162
+ );
163
+ if (error) {
164
+ throw new GarminApiError(
165
+ `Garmin API error: ${response.status} ${response.statusText}`,
166
+ response.status,
167
+ JSON.stringify(error),
168
+ );
169
+ }
170
+ return (data ?? []) as GarminActivity[];
171
+ }
172
+
173
+ // ─── Activity Details ─────────────────────────────────────────────────
174
+
175
+ /**
176
+ * Get detailed activity summaries including GPS, heart rate, and sensor data.
177
+ *
178
+ * Garmin API: `GET /wellness-api/rest/activityDetails`
179
+ */
180
+ async getActivityDetails(
181
+ params: TimeRangeParams,
182
+ ): Promise<GarminActivityDetail[]> {
183
+ const { data, error, response } = await this.wellness.GET(
184
+ "/rest/activityDetails",
185
+ {
186
+ params: { query: timeRangeQuery(params, this.accessToken) },
187
+ },
80
188
  );
189
+ if (error) {
190
+ throw new GarminApiError(
191
+ `Garmin API error: ${response.status} ${response.statusText}`,
192
+ response.status,
193
+ JSON.stringify(error),
194
+ );
195
+ }
196
+ return (data ?? []) as GarminActivityDetail[];
197
+ }
198
+
199
+ // ─── Activity File ────────────────────────────────────────────────────
200
+
201
+ /**
202
+ * Download a raw activity file (FIT, TCX, or GPX).
203
+ *
204
+ * Garmin API: `GET /wellness-api/rest/activityFile`
205
+ *
206
+ * @returns The raw file as an ArrayBuffer.
207
+ */
208
+ async getActivityFile(id: string): Promise<ArrayBuffer> {
209
+ const url = `${this.baseUrl}/wellness-api/rest/activityFile?id=${encodeURIComponent(id)}`;
210
+ const response = await fetch(url, {
211
+ method: "GET",
212
+ headers: {
213
+ Authorization: `Bearer ${this.accessToken}`,
214
+ },
215
+ });
216
+
217
+ if (!response.ok) {
218
+ const body = await response.text().catch(() => "");
219
+ throw new GarminApiError(
220
+ `Garmin API error: ${response.status} ${response.statusText}`,
221
+ response.status,
222
+ body,
223
+ );
224
+ }
225
+
226
+ return response.arrayBuffer();
81
227
  }
82
228
 
83
- // ─── Sleep ──────────────────────────────────────────────────────────────
229
+ // ─── Sleep ────────────────────────────────────────────────────────────
84
230
 
85
231
  /**
86
232
  * Get sleep summaries.
87
233
  *
88
234
  * Garmin API: `GET /wellness-api/rest/sleeps`
89
235
  */
90
- async getSleeps(params: TimeRangeParams): Promise<GarminSleep[]> {
91
- return this.get<GarminSleep[]>(
92
- "/wellness-api/rest/sleeps",
93
- timeRangeQuery(params),
94
- );
236
+ async getSleeps(params: TimeRangeParams): Promise<GarminSleepExtended[]> {
237
+ const { data, error, response } = await this.wellness.GET("/rest/sleeps", {
238
+ params: { query: timeRangeQuery(params, this.accessToken) },
239
+ });
240
+ if (error) {
241
+ throw new GarminApiError(
242
+ `Garmin API error: ${response.status} ${response.statusText}`,
243
+ response.status,
244
+ JSON.stringify(error),
245
+ );
246
+ }
247
+ return (data ?? []) as GarminSleepExtended[];
95
248
  }
96
249
 
97
250
  // ─── Body Composition ─────────────────────────────────────────────────
@@ -104,26 +257,341 @@ export class GarminClient {
104
257
  async getBodyCompositions(
105
258
  params: TimeRangeParams,
106
259
  ): Promise<GarminBodyComposition[]> {
107
- return this.get<GarminBodyComposition[]>(
108
- "/wellness-api/rest/bodyComps",
109
- timeRangeQuery(params),
260
+ const { data, error, response } = await this.wellness.GET(
261
+ "/rest/bodyComps",
262
+ {
263
+ params: { query: timeRangeQuery(params, this.accessToken) },
264
+ },
110
265
  );
266
+ if (error) {
267
+ throw new GarminApiError(
268
+ `Garmin API error: ${response.status} ${response.statusText}`,
269
+ response.status,
270
+ JSON.stringify(error),
271
+ );
272
+ }
273
+ return (data ?? []) as GarminBodyComposition[];
111
274
  }
112
275
 
113
276
  // ─── Menstrual Cycle ──────────────────────────────────────────────────
114
277
 
115
278
  /**
116
- * Get menstrual cycle data.
279
+ * Get menstrual cycle tracking data.
117
280
  *
118
- * Garmin API: `GET /wellness-api/rest/menstrualCycleData`
281
+ * Garmin API: `GET /wellness-api/rest/mct`
119
282
  */
120
283
  async getMenstrualCycleData(
121
284
  params: TimeRangeParams,
122
- ): Promise<GarminMenstrualCycleData[]> {
123
- return this.get<GarminMenstrualCycleData[]>(
124
- "/wellness-api/rest/menstrualCycleData",
125
- timeRangeQuery(params),
285
+ ): Promise<GarminMenstrualCycle[]> {
286
+ const { data, error, response } = await this.wellness.GET("/rest/mct", {
287
+ params: { query: timeRangeQuery(params, this.accessToken) },
288
+ });
289
+ if (error) {
290
+ throw new GarminApiError(
291
+ `Garmin API error: ${response.status} ${response.statusText}`,
292
+ response.status,
293
+ JSON.stringify(error),
294
+ );
295
+ }
296
+ return (data ?? []) as GarminMenstrualCycle[];
297
+ }
298
+
299
+ // ─── User Metrics ─────────────────────────────────────────────────────
300
+
301
+ /**
302
+ * Get user metrics (VO2 max, fitness age, etc.).
303
+ *
304
+ * Garmin API: `GET /wellness-api/rest/userMetrics`
305
+ */
306
+ async getUserMetrics(
307
+ params: TimeRangeParams,
308
+ ): Promise<GarminUserMetrics[]> {
309
+ const { data, error, response } = await this.wellness.GET(
310
+ "/rest/userMetrics",
311
+ {
312
+ params: { query: timeRangeQuery(params, this.accessToken) },
313
+ },
314
+ );
315
+ if (error) {
316
+ throw new GarminApiError(
317
+ `Garmin API error: ${response.status} ${response.statusText}`,
318
+ response.status,
319
+ JSON.stringify(error),
320
+ );
321
+ }
322
+ return (data ?? []) as GarminUserMetrics[];
323
+ }
324
+
325
+ // ─── Stress Details ───────────────────────────────────────────────────
326
+
327
+ /**
328
+ * Get stress detail summaries.
329
+ *
330
+ * Garmin API: `GET /wellness-api/rest/stressDetails`
331
+ */
332
+ async getStressDetails(
333
+ params: TimeRangeParams,
334
+ ): Promise<GarminStressDetail[]> {
335
+ const { data, error, response } = await this.wellness.GET(
336
+ "/rest/stressDetails",
337
+ {
338
+ params: { query: timeRangeQuery(params, this.accessToken) },
339
+ },
340
+ );
341
+ if (error) {
342
+ throw new GarminApiError(
343
+ `Garmin API error: ${response.status} ${response.statusText}`,
344
+ response.status,
345
+ JSON.stringify(error),
346
+ );
347
+ }
348
+ return (data ?? []) as GarminStressDetail[];
349
+ }
350
+
351
+ // ─── Skin Temperature ─────────────────────────────────────────────────
352
+
353
+ /**
354
+ * Get skin temperature summaries.
355
+ *
356
+ * Garmin API: `GET /wellness-api/rest/skinTemp`
357
+ */
358
+ async getSkinTemperature(
359
+ params: TimeRangeParams,
360
+ ): Promise<GarminSkinTemperature[]> {
361
+ const { data, error, response } = await this.wellness.GET(
362
+ "/rest/skinTemp",
363
+ {
364
+ params: { query: timeRangeQuery(params, this.accessToken) },
365
+ },
366
+ );
367
+ if (error) {
368
+ throw new GarminApiError(
369
+ `Garmin API error: ${response.status} ${response.statusText}`,
370
+ response.status,
371
+ JSON.stringify(error),
372
+ );
373
+ }
374
+ return (data ?? []) as GarminSkinTemperature[];
375
+ }
376
+
377
+ // ─── Respiration ──────────────────────────────────────────────────────
378
+
379
+ /**
380
+ * Get respiration summaries.
381
+ *
382
+ * Garmin API: `GET /wellness-api/rest/respiration`
383
+ */
384
+ async getRespiration(
385
+ params: TimeRangeParams,
386
+ ): Promise<GarminRespiration[]> {
387
+ const { data, error, response } = await this.wellness.GET(
388
+ "/rest/respiration",
389
+ {
390
+ params: { query: timeRangeQuery(params, this.accessToken) },
391
+ },
392
+ );
393
+ if (error) {
394
+ throw new GarminApiError(
395
+ `Garmin API error: ${response.status} ${response.statusText}`,
396
+ response.status,
397
+ JSON.stringify(error),
398
+ );
399
+ }
400
+ return (data ?? []) as GarminRespiration[];
401
+ }
402
+
403
+ // ─── Pulse Ox ─────────────────────────────────────────────────────────
404
+
405
+ /**
406
+ * Get pulse oximetry (SpO2) summaries.
407
+ *
408
+ * Garmin API: `GET /wellness-api/rest/pulseOx`
409
+ */
410
+ async getPulseOx(params: TimeRangeParams): Promise<GarminPulseOx[]> {
411
+ const { data, error, response } = await this.wellness.GET(
412
+ "/rest/pulseOx",
413
+ {
414
+ params: { query: timeRangeQuery(params, this.accessToken) },
415
+ },
416
+ );
417
+ if (error) {
418
+ throw new GarminApiError(
419
+ `Garmin API error: ${response.status} ${response.statusText}`,
420
+ response.status,
421
+ JSON.stringify(error),
422
+ );
423
+ }
424
+ return (data ?? []) as GarminPulseOx[];
425
+ }
426
+
427
+ // ─── Move IQ ──────────────────────────────────────────────────────────
428
+
429
+ /**
430
+ * Get Move IQ auto-detected activity events.
431
+ *
432
+ * Garmin API: `GET /wellness-api/rest/moveiq`
433
+ */
434
+ async getMoveIQ(params: TimeRangeParams): Promise<GarminMoveIQEvent[]> {
435
+ const { data, error, response } = await this.wellness.GET(
436
+ "/rest/moveiq",
437
+ {
438
+ params: { query: timeRangeQuery(params, this.accessToken) },
439
+ },
440
+ );
441
+ if (error) {
442
+ throw new GarminApiError(
443
+ `Garmin API error: ${response.status} ${response.statusText}`,
444
+ response.status,
445
+ JSON.stringify(error),
446
+ );
447
+ }
448
+ return (data ?? []) as GarminMoveIQEvent[];
449
+ }
450
+
451
+ // ─── HRV ──────────────────────────────────────────────────────────────
452
+
453
+ /**
454
+ * Get heart rate variability (HRV) summaries.
455
+ *
456
+ * Garmin API: `GET /wellness-api/rest/hrv`
457
+ */
458
+ async getHRV(params: TimeRangeParams): Promise<GarminHRVSummary[]> {
459
+ const { data, error, response } = await this.wellness.GET("/rest/hrv", {
460
+ params: { query: timeRangeQuery(params, this.accessToken) },
461
+ });
462
+ if (error) {
463
+ throw new GarminApiError(
464
+ `Garmin API error: ${response.status} ${response.statusText}`,
465
+ response.status,
466
+ JSON.stringify(error),
467
+ );
468
+ }
469
+ return (data ?? []) as GarminHRVSummary[];
470
+ }
471
+
472
+ // ─── Health Snapshot ──────────────────────────────────────────────────
473
+
474
+ /**
475
+ * Get health snapshot summaries.
476
+ *
477
+ * Garmin API: `GET /wellness-api/rest/healthSnapshot`
478
+ */
479
+ async getHealthSnapshot(
480
+ params: TimeRangeParams,
481
+ ): Promise<GarminHealthSnapshot[]> {
482
+ const { data, error, response } = await this.wellness.GET(
483
+ "/rest/healthSnapshot",
484
+ {
485
+ params: { query: timeRangeQuery(params, this.accessToken) },
486
+ },
487
+ );
488
+ if (error) {
489
+ throw new GarminApiError(
490
+ `Garmin API error: ${response.status} ${response.statusText}`,
491
+ response.status,
492
+ JSON.stringify(error),
493
+ );
494
+ }
495
+ return (data ?? []) as GarminHealthSnapshot[];
496
+ }
497
+
498
+ // ─── Epochs ───────────────────────────────────────────────────────────
499
+
500
+ /**
501
+ * Get epoch (15-minute interval) summaries.
502
+ *
503
+ * Garmin API: `GET /wellness-api/rest/epochs`
504
+ */
505
+ async getEpochs(params: TimeRangeParams): Promise<GarminEpoch[]> {
506
+ const { data, error, response } = await this.wellness.GET("/rest/epochs", {
507
+ params: { query: timeRangeQuery(params, this.accessToken) },
508
+ });
509
+ if (error) {
510
+ throw new GarminApiError(
511
+ `Garmin API error: ${response.status} ${response.statusText}`,
512
+ response.status,
513
+ JSON.stringify(error),
514
+ );
515
+ }
516
+ return (data ?? []) as GarminEpoch[];
517
+ }
518
+
519
+ // ─── Blood Pressure ───────────────────────────────────────────────────
520
+
521
+ /**
522
+ * Get blood pressure summaries.
523
+ *
524
+ * Garmin API: `GET /wellness-api/rest/bloodPressures`
525
+ */
526
+ async getBloodPressures(
527
+ params: TimeRangeParams,
528
+ ): Promise<GarminBloodPressure[]> {
529
+ const { data, error, response } = await this.wellness.GET(
530
+ "/rest/bloodPressures",
531
+ {
532
+ params: { query: timeRangeQuery(params, this.accessToken) },
533
+ },
534
+ );
535
+ if (error) {
536
+ throw new GarminApiError(
537
+ `Garmin API error: ${response.status} ${response.statusText}`,
538
+ response.status,
539
+ JSON.stringify(error),
540
+ );
541
+ }
542
+ return (data ?? []) as GarminBloodPressure[];
543
+ }
544
+
545
+ // ─── Manually Updated Activities ──────────────────────────────────────
546
+
547
+ /**
548
+ * Get manually created or edited activities.
549
+ *
550
+ * Garmin API: `GET /wellness-api/rest/manuallyUpdatedActivities`
551
+ */
552
+ async getManuallyUpdatedActivities(
553
+ params: TimeRangeParams,
554
+ ): Promise<GarminActivity[]> {
555
+ const { data, error, response } = await this.wellness.GET(
556
+ "/rest/manuallyUpdatedActivities",
557
+ {
558
+ params: { query: timeRangeQuery(params, this.accessToken) },
559
+ },
560
+ );
561
+ if (error) {
562
+ throw new GarminApiError(
563
+ `Garmin API error: ${response.status} ${response.statusText}`,
564
+ response.status,
565
+ JSON.stringify(error),
566
+ );
567
+ }
568
+ return (data ?? []) as GarminActivity[];
569
+ }
570
+
571
+ // ─── Solar Intensity ──────────────────────────────────────────────────
572
+
573
+ /**
574
+ * Get solar intensity summaries.
575
+ *
576
+ * Garmin API: `GET /wellness-api/rest/solarIntensity`
577
+ */
578
+ async getSolarIntensity(
579
+ params: TimeRangeParams,
580
+ ): Promise<GarminSolar[]> {
581
+ const { data, error, response } = await this.wellness.GET(
582
+ "/rest/solarIntensity",
583
+ {
584
+ params: { query: timeRangeQuery(params, this.accessToken) },
585
+ },
126
586
  );
587
+ if (error) {
588
+ throw new GarminApiError(
589
+ `Garmin API error: ${response.status} ${response.statusText}`,
590
+ response.status,
591
+ JSON.stringify(error),
592
+ );
593
+ }
594
+ return (data ?? []) as GarminSolar[];
127
595
  }
128
596
 
129
597
  // ─── Backfill ─────────────────────────────────────────────────────────
@@ -134,6 +602,10 @@ export class GarminClient {
134
602
  * Garmin processes backfill requests asynchronously and delivers data
135
603
  * via the configured webhook endpoint. Maximum range: 90 days per request.
136
604
  *
605
+ * Uses manual fetch because the spec defines individual backfill paths
606
+ * (e.g. `/rest/backfill/dailies`) but this method accepts a dynamic
607
+ * summaryType string.
608
+ *
137
609
  * @param summaryType - The data type to backfill (e.g., "dailies", "activities", "sleeps", "bodyComps")
138
610
  * @param params - Time range for the backfill
139
611
  */
@@ -141,24 +613,54 @@ export class GarminClient {
141
613
  summaryType: string,
142
614
  params: TimeRangeParams,
143
615
  ): Promise<void> {
144
- const query = timeRangeQuery(params);
145
- await this.get(
146
- `/wellness-api/rest/backfill/${summaryType}`,
147
- query,
148
- );
616
+ const query = timeRangeQuery(params, this.accessToken);
617
+ const qs = new URLSearchParams(query).toString();
618
+ const url = `${this.baseUrl}/wellness-api/rest/backfill/${summaryType}?${qs}`;
619
+
620
+ const response = await fetch(url, {
621
+ method: "GET",
622
+ headers: {
623
+ Authorization: `Bearer ${this.accessToken}`,
624
+ Accept: "application/json",
625
+ },
626
+ });
627
+
628
+ if (!response.ok) {
629
+ const body = await response.text().catch(() => "");
630
+ throw new GarminApiError(
631
+ `Garmin API error: ${response.status} ${response.statusText}`,
632
+ response.status,
633
+ body,
634
+ );
635
+ }
149
636
  }
150
637
 
151
- // ─── Training API V2 ────────────────────────────────────────────────
638
+ // ─── User Deregistration ──────────────────────────────────────────────
152
639
 
153
640
  /**
154
- * Check which permissions the user has granted.
641
+ * Delete the user's registration with Garmin.
155
642
  *
156
- * Garmin OAuth 2.0 PKCE spec: `GET /wellness-api/rest/user/permissions`
643
+ * Must be called when the user disconnects or deletes their account
644
+ * to comply with Garmin's API requirements.
157
645
  */
158
- async getUserPermissions(): Promise<string[]> {
159
- return this.get<string[]>("/wellness-api/rest/user/permissions");
646
+ async deleteUserRegistration(): Promise<void> {
647
+ const { error, response } = await this.wellness.DELETE(
648
+ "/rest/user/registration",
649
+ );
650
+
651
+ if (error) {
652
+ throw new GarminApiError(
653
+ `Garmin API error: ${response.status} ${response.statusText}`,
654
+ response.status,
655
+ JSON.stringify(error),
656
+ );
657
+ }
160
658
  }
161
659
 
660
+ // ─── Training API V2 ─────────────────────────────────────────────────
661
+ // These endpoints are NOT part of the Wellness API spec, so they use
662
+ // manual fetch.
663
+
162
664
  /**
163
665
  * Create a workout in Garmin Connect.
164
666
  *
@@ -243,34 +745,7 @@ export class GarminClient {
243
745
  await this.del(`/training-api/schedule/${scheduleId}`);
244
746
  }
245
747
 
246
- // ─── User Deregistration ──────────────────────────────────────────────
247
-
248
- /**
249
- * Delete the user's registration with Garmin.
250
- *
251
- * Must be called when the user disconnects or deletes their account
252
- * to comply with Garmin's API requirements.
253
- */
254
- async deleteUserRegistration(): Promise<void> {
255
- const url = `${this.baseUrl}/wellness-api/rest/user/registration`;
256
- const response = await fetch(url, {
257
- method: "DELETE",
258
- headers: {
259
- Authorization: `Bearer ${this.accessToken}`,
260
- },
261
- });
262
-
263
- if (!response.ok) {
264
- const body = await response.text().catch(() => "");
265
- throw new GarminApiError(
266
- `Garmin API error: ${response.status} ${response.statusText}`,
267
- response.status,
268
- body,
269
- );
270
- }
271
- }
272
-
273
- // ─── Internal ─────────────────────────────────────────────────────────
748
+ // ─── Internal (Training API helpers) ──────────────────────────────────
274
749
 
275
750
  private async get<T>(
276
751
  path: string,
@@ -386,10 +861,14 @@ export interface TimeRangeParams {
386
861
  uploadEndTimeInSeconds: number;
387
862
  }
388
863
 
389
- function timeRangeQuery(params: TimeRangeParams): Record<string, string> {
864
+ function timeRangeQuery(
865
+ params: TimeRangeParams,
866
+ token?: string,
867
+ ): { uploadStartTimeInSeconds: string; uploadEndTimeInSeconds: string; token?: string } {
390
868
  return {
391
869
  uploadStartTimeInSeconds: String(params.uploadStartTimeInSeconds),
392
870
  uploadEndTimeInSeconds: String(params.uploadEndTimeInSeconds),
871
+ ...(token ? { token } : {}),
393
872
  };
394
873
  }
395
874
 
@@ -401,7 +880,7 @@ export class GarminApiError extends Error {
401
880
  public readonly status: number,
402
881
  public readonly body: string,
403
882
  ) {
404
- super(message);
883
+ super(`${message} — ${body}`);
405
884
  this.name = "GarminApiError";
406
885
  }
407
886
  }