@clipin/convex-wearables 0.0.1

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 (143) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +616 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +4 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/index.d.ts +244 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +555 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/types.d.ts +689 -0
  12. package/dist/client/types.d.ts.map +1 -0
  13. package/dist/client/types.js +112 -0
  14. package/dist/client/types.js.map +1 -0
  15. package/dist/component/_generated/_ignore.d.ts +1 -0
  16. package/dist/component/_generated/_ignore.d.ts.map +1 -0
  17. package/dist/component/_generated/_ignore.js +4 -0
  18. package/dist/component/_generated/_ignore.js.map +1 -0
  19. package/dist/component/_generated/api.d.ts +13 -0
  20. package/dist/component/_generated/api.d.ts.map +1 -0
  21. package/dist/component/_generated/api.js +14 -0
  22. package/dist/component/_generated/api.js.map +1 -0
  23. package/dist/component/_generated/dataModel.d.ts +28 -0
  24. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  25. package/dist/component/_generated/dataModel.js +11 -0
  26. package/dist/component/_generated/dataModel.js.map +1 -0
  27. package/dist/component/_generated/server.d.ts +23 -0
  28. package/dist/component/_generated/server.d.ts.map +1 -0
  29. package/dist/component/_generated/server.js +18 -0
  30. package/dist/component/_generated/server.js.map +1 -0
  31. package/dist/component/backfillJobs.d.ts +121 -0
  32. package/dist/component/backfillJobs.d.ts.map +1 -0
  33. package/dist/component/backfillJobs.js +233 -0
  34. package/dist/component/backfillJobs.js.map +1 -0
  35. package/dist/component/connections.d.ts +159 -0
  36. package/dist/component/connections.d.ts.map +1 -0
  37. package/dist/component/connections.js +288 -0
  38. package/dist/component/connections.js.map +1 -0
  39. package/dist/component/convex.config.d.ts +3 -0
  40. package/dist/component/convex.config.d.ts.map +1 -0
  41. package/dist/component/convex.config.js +6 -0
  42. package/dist/component/convex.config.js.map +1 -0
  43. package/dist/component/dataPoints.d.ts +81 -0
  44. package/dist/component/dataPoints.d.ts.map +1 -0
  45. package/dist/component/dataPoints.js +258 -0
  46. package/dist/component/dataPoints.js.map +1 -0
  47. package/dist/component/dataSources.d.ts +56 -0
  48. package/dist/component/dataSources.d.ts.map +1 -0
  49. package/dist/component/dataSources.js +95 -0
  50. package/dist/component/dataSources.js.map +1 -0
  51. package/dist/component/events.d.ts +203 -0
  52. package/dist/component/events.d.ts.map +1 -0
  53. package/dist/component/events.js +251 -0
  54. package/dist/component/events.js.map +1 -0
  55. package/dist/component/garminBackfill.d.ts +40 -0
  56. package/dist/component/garminBackfill.d.ts.map +1 -0
  57. package/dist/component/garminBackfill.js +296 -0
  58. package/dist/component/garminBackfill.js.map +1 -0
  59. package/dist/component/garminWebhooks.d.ts +17 -0
  60. package/dist/component/garminWebhooks.d.ts.map +1 -0
  61. package/dist/component/garminWebhooks.js +505 -0
  62. package/dist/component/garminWebhooks.js.map +1 -0
  63. package/dist/component/httpHandlers.d.ts +32 -0
  64. package/dist/component/httpHandlers.d.ts.map +1 -0
  65. package/dist/component/httpHandlers.js +131 -0
  66. package/dist/component/httpHandlers.js.map +1 -0
  67. package/dist/component/lifecycle.d.ts +12 -0
  68. package/dist/component/lifecycle.d.ts.map +1 -0
  69. package/dist/component/lifecycle.js +79 -0
  70. package/dist/component/lifecycle.js.map +1 -0
  71. package/dist/component/menstrualCycles.d.ts +98 -0
  72. package/dist/component/menstrualCycles.d.ts.map +1 -0
  73. package/dist/component/menstrualCycles.js +112 -0
  74. package/dist/component/menstrualCycles.js.map +1 -0
  75. package/dist/component/oauthActions.d.ts +52 -0
  76. package/dist/component/oauthActions.d.ts.map +1 -0
  77. package/dist/component/oauthActions.js +208 -0
  78. package/dist/component/oauthActions.js.map +1 -0
  79. package/dist/component/oauthStates.d.ts +47 -0
  80. package/dist/component/oauthStates.d.ts.map +1 -0
  81. package/dist/component/oauthStates.js +77 -0
  82. package/dist/component/oauthStates.js.map +1 -0
  83. package/dist/component/providerSettings.d.ts +15 -0
  84. package/dist/component/providerSettings.d.ts.map +1 -0
  85. package/dist/component/providerSettings.js +57 -0
  86. package/dist/component/providerSettings.js.map +1 -0
  87. package/dist/component/providers/garmin.d.ts +306 -0
  88. package/dist/component/providers/garmin.d.ts.map +1 -0
  89. package/dist/component/providers/garmin.js +675 -0
  90. package/dist/component/providers/garmin.js.map +1 -0
  91. package/dist/component/providers/oauth.d.ts +42 -0
  92. package/dist/component/providers/oauth.d.ts.map +1 -0
  93. package/dist/component/providers/oauth.js +181 -0
  94. package/dist/component/providers/oauth.js.map +1 -0
  95. package/dist/component/providers/polar.d.ts +6 -0
  96. package/dist/component/providers/polar.d.ts.map +1 -0
  97. package/dist/component/providers/polar.js +175 -0
  98. package/dist/component/providers/polar.js.map +1 -0
  99. package/dist/component/providers/registry.d.ts +14 -0
  100. package/dist/component/providers/registry.d.ts.map +1 -0
  101. package/dist/component/providers/registry.js +32 -0
  102. package/dist/component/providers/registry.js.map +1 -0
  103. package/dist/component/providers/strava.d.ts +45 -0
  104. package/dist/component/providers/strava.d.ts.map +1 -0
  105. package/dist/component/providers/strava.js +182 -0
  106. package/dist/component/providers/strava.js.map +1 -0
  107. package/dist/component/providers/suunto.d.ts +5 -0
  108. package/dist/component/providers/suunto.d.ts.map +1 -0
  109. package/dist/component/providers/suunto.js +502 -0
  110. package/dist/component/providers/suunto.js.map +1 -0
  111. package/dist/component/providers/types.d.ts +139 -0
  112. package/dist/component/providers/types.d.ts.map +1 -0
  113. package/dist/component/providers/types.js +5 -0
  114. package/dist/component/providers/types.js.map +1 -0
  115. package/dist/component/providers/whoop.d.ts +4 -0
  116. package/dist/component/providers/whoop.d.ts.map +1 -0
  117. package/dist/component/providers/whoop.js +439 -0
  118. package/dist/component/providers/whoop.js.map +1 -0
  119. package/dist/component/schema.d.ts +429 -0
  120. package/dist/component/schema.d.ts.map +1 -0
  121. package/dist/component/schema.js +282 -0
  122. package/dist/component/schema.js.map +1 -0
  123. package/dist/component/sdkPush.d.ts +143 -0
  124. package/dist/component/sdkPush.d.ts.map +1 -0
  125. package/dist/component/sdkPush.js +338 -0
  126. package/dist/component/sdkPush.js.map +1 -0
  127. package/dist/component/summaries.d.ts +129 -0
  128. package/dist/component/summaries.d.ts.map +1 -0
  129. package/dist/component/summaries.js +129 -0
  130. package/dist/component/summaries.js.map +1 -0
  131. package/dist/component/syncJobs.d.ts +142 -0
  132. package/dist/component/syncJobs.d.ts.map +1 -0
  133. package/dist/component/syncJobs.js +136 -0
  134. package/dist/component/syncJobs.js.map +1 -0
  135. package/dist/component/syncWorkflow.d.ts +99 -0
  136. package/dist/component/syncWorkflow.d.ts.map +1 -0
  137. package/dist/component/syncWorkflow.js +579 -0
  138. package/dist/component/syncWorkflow.js.map +1 -0
  139. package/dist/component/workflowManager.d.ts +3 -0
  140. package/dist/component/workflowManager.d.ts.map +1 -0
  141. package/dist/component/workflowManager.js +17 -0
  142. package/dist/component/workflowManager.js.map +1 -0
  143. package/package.json +84 -0
@@ -0,0 +1,675 @@
1
+ /**
2
+ * Garmin provider adapter.
3
+ *
4
+ * Garmin is a push-based provider: after OAuth 2.0 authorization (with PKCE),
5
+ * Garmin pushes data to registered webhook endpoints.
6
+ *
7
+ * This module handles:
8
+ * - OAuth config (OAuth 2.0 + PKCE)
9
+ * - Activity normalization (push webhook payloads)
10
+ * - Sleep normalization
11
+ * - Daily summary normalization
12
+ * - Epoch (15-min interval) normalization
13
+ * - Body composition normalization
14
+ */
15
+ import { makeAuthenticatedRequest } from "./oauth";
16
+ // ---------------------------------------------------------------------------
17
+ // OAuth config
18
+ // ---------------------------------------------------------------------------
19
+ const API_BASE = "https://apis.garmin.com";
20
+ export function garminOAuthConfig(credentials) {
21
+ return {
22
+ endpoints: {
23
+ authorizeUrl: "https://connect.garmin.com/oauth2Confirm",
24
+ tokenUrl: "https://connectapi.garmin.com/di-oauth2-service/oauth/token",
25
+ apiBaseUrl: API_BASE,
26
+ },
27
+ clientId: credentials.clientId,
28
+ clientSecret: credentials.clientSecret,
29
+ defaultScope: "", // Scope is managed at app creation in Garmin Developer Portal
30
+ usePkce: true,
31
+ authMethod: "body",
32
+ };
33
+ }
34
+ function isoDateFromTimestamp(timestampMs) {
35
+ return new Date(timestampMs).toISOString().split("T")[0] ?? "";
36
+ }
37
+ function isoDateFromCalendarDate(calendarDate, fallbackTimestampMs) {
38
+ return calendarDate ?? isoDateFromTimestamp(fallbackTimestampMs);
39
+ }
40
+ function calendarDateToMiddayTimestamp(calendarDate) {
41
+ if (!calendarDate) {
42
+ return null;
43
+ }
44
+ const timestamp = Date.parse(`${calendarDate}T12:00:00Z`);
45
+ return Number.isFinite(timestamp) ? timestamp : null;
46
+ }
47
+ function buildOffsetDataPoints(values, startTimeInSeconds, seriesType, externalIdPrefix) {
48
+ if (!values) {
49
+ return [];
50
+ }
51
+ return Object.entries(values).reduce((points, [offsetStr, value]) => {
52
+ const offsetSeconds = Number(offsetStr);
53
+ if (!Number.isFinite(offsetSeconds) || !Number.isFinite(value)) {
54
+ return points;
55
+ }
56
+ points.push({
57
+ seriesType,
58
+ recordedAt: (startTimeInSeconds + offsetSeconds) * 1000,
59
+ value,
60
+ externalId: externalIdPrefix ? `${externalIdPrefix}:${offsetStr}` : undefined,
61
+ });
62
+ return points;
63
+ }, []);
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Workout type mapping (Garmin activityType → unified type)
67
+ // ---------------------------------------------------------------------------
68
+ const WORKOUT_TYPE_MAP = {
69
+ RUNNING: "running",
70
+ TRAIL_RUNNING: "trail_running",
71
+ TREADMILL_RUNNING: "treadmill",
72
+ VIRTUAL_RUN: "running",
73
+ INDOOR_RUNNING: "treadmill",
74
+ CYCLING: "cycling",
75
+ MOUNTAIN_BIKING: "mountain_biking",
76
+ GRAVEL_CYCLING: "cycling",
77
+ INDOOR_CYCLING: "indoor_cycling",
78
+ VIRTUAL_RIDE: "indoor_cycling",
79
+ ROAD_BIKING: "cycling",
80
+ BMX: "cycling",
81
+ RECUMBENT_CYCLING: "cycling",
82
+ E_BIKE_MOUNTAIN: "e_biking",
83
+ E_BIKE_FITNESS: "e_biking",
84
+ SWIMMING: "swimming",
85
+ LAP_SWIMMING: "swimming",
86
+ OPEN_WATER_SWIMMING: "open_water_swimming",
87
+ POOL_SWIM: "swimming",
88
+ HIKING: "hiking",
89
+ WALKING: "walking",
90
+ CASUAL_WALKING: "walking",
91
+ SPEED_WALKING: "walking",
92
+ YOGA: "yoga",
93
+ PILATES: "pilates",
94
+ STRENGTH_TRAINING: "strength_training",
95
+ CARDIO_TRAINING: "cardio_training",
96
+ ELLIPTICAL: "elliptical",
97
+ STAIR_CLIMBING: "stair_climbing",
98
+ INDOOR_ROWING: "rowing_machine",
99
+ ROWING: "rowing",
100
+ KAYAKING: "kayaking",
101
+ STAND_UP_PADDLEBOARDING: "stand_up_paddleboarding",
102
+ SURFING: "surfing",
103
+ KITEBOARDING: "kitesurfing",
104
+ WINDSURFING: "windsurfing",
105
+ SAILING: "sailing",
106
+ TENNIS: "tennis",
107
+ TABLE_TENNIS: "table_tennis",
108
+ PICKLEBALL: "pickleball",
109
+ BADMINTON: "badminton",
110
+ SQUASH: "squash",
111
+ RACQUETBALL: "squash",
112
+ PADEL: "padel",
113
+ SOCCER: "soccer",
114
+ BASKETBALL: "basketball",
115
+ VOLLEYBALL: "volleyball",
116
+ FOOTBALL: "football",
117
+ BASEBALL: "baseball",
118
+ SOFTBALL: "softball",
119
+ RUGBY: "rugby",
120
+ HOCKEY: "hockey",
121
+ LACROSSE: "lacrosse",
122
+ CRICKET: "cricket",
123
+ GOLF: "golf",
124
+ DISC_GOLF: "golf",
125
+ ROCK_CLIMBING: "rock_climbing",
126
+ BOULDERING: "rock_climbing",
127
+ INDOOR_CLIMBING: "rock_climbing",
128
+ SKATEBOARDING: "skateboarding",
129
+ INLINE_SKATING: "inline_skating",
130
+ ICE_SKATING: "ice_skating",
131
+ SKIING: "alpine_skiing",
132
+ RESORT_SKIING_SNOWBOARDING: "alpine_skiing",
133
+ BACKCOUNTRY_SKIING_SNOWBOARDING: "backcountry_skiing",
134
+ CROSS_COUNTRY_SKIING: "cross_country_skiing",
135
+ SNOWBOARDING: "snowboarding",
136
+ SNOWSHOEING: "snowshoeing",
137
+ BOXING: "boxing",
138
+ MARTIAL_ARTS: "martial_arts",
139
+ JUMP_ROPE: "jump_rope",
140
+ HIIT: "cardio_training",
141
+ FITNESS_EQUIPMENT: "cardio_training",
142
+ BREATHWORK: "breathwork",
143
+ MEDITATION: "meditation",
144
+ OTHER: "other",
145
+ };
146
+ function getUnifiedWorkoutType(activityType) {
147
+ return WORKOUT_TYPE_MAP[activityType] ?? "other";
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // Normalization — Activities
151
+ // ---------------------------------------------------------------------------
152
+ export function normalizeActivity(activity) {
153
+ const startMs = activity.startTimeInSeconds * 1000;
154
+ const endMs = startMs + activity.durationInSeconds * 1000;
155
+ return {
156
+ category: "workout",
157
+ type: getUnifiedWorkoutType(activity.activityType),
158
+ sourceName: activity.deviceName ?? "Garmin",
159
+ deviceModel: activity.deviceName,
160
+ durationSeconds: activity.durationInSeconds,
161
+ startDatetime: startMs,
162
+ endDatetime: endMs,
163
+ externalId: `garmin-${activity.activityId}`,
164
+ heartRateAvg: activity.averageHeartRateInBeatsPerMinute,
165
+ heartRateMax: activity.maxHeartRateInBeatsPerMinute,
166
+ energyBurned: activity.activeKilocalories,
167
+ distance: activity.distanceInMeters,
168
+ stepsCount: activity.steps,
169
+ averageSpeed: activity.averageSpeedInMetersPerSecond,
170
+ maxSpeed: activity.maxSpeedInMetersPerSecond,
171
+ averageWatts: activity.averagePowerInWatts,
172
+ maxWatts: activity.maxPowerInWatts,
173
+ totalElevationGain: activity.totalElevationGainInMeters,
174
+ movingTimeSeconds: activity.movingDurationInSeconds,
175
+ };
176
+ }
177
+ // ---------------------------------------------------------------------------
178
+ // Normalization — Sleep
179
+ // ---------------------------------------------------------------------------
180
+ export function normalizeSleep(sleep) {
181
+ const startMs = sleep.startTimeInSeconds * 1000;
182
+ const endMs = startMs + sleep.durationInSeconds * 1000;
183
+ // Build sleep stages from sleepLevelsMap if available
184
+ let sleepStages;
185
+ if (sleep.sleepLevelsMap) {
186
+ sleepStages = [];
187
+ const stageMapping = {
188
+ deep: "deep",
189
+ light: "light",
190
+ rem: "rem",
191
+ awake: "awake",
192
+ };
193
+ for (const [key, intervals] of Object.entries(sleep.sleepLevelsMap)) {
194
+ const stage = stageMapping[key] ?? key;
195
+ for (const interval of intervals) {
196
+ sleepStages.push({
197
+ stage,
198
+ startTime: interval.startTimeInSeconds * 1000,
199
+ endTime: interval.endTimeInSeconds * 1000,
200
+ });
201
+ }
202
+ }
203
+ // Sort by start time
204
+ sleepStages.sort((a, b) => a.startTime - b.startTime);
205
+ }
206
+ return {
207
+ category: "sleep",
208
+ type: "sleep_session",
209
+ sourceName: "Garmin",
210
+ durationSeconds: sleep.durationInSeconds,
211
+ startDatetime: startMs,
212
+ endDatetime: endMs,
213
+ externalId: `garmin-sleep-${sleep.summaryId}`,
214
+ heartRateAvg: sleep.averageHeartRate,
215
+ heartRateMin: sleep.lowestHeartRate,
216
+ sleepTotalDurationMinutes: Math.floor(sleep.durationInSeconds / 60),
217
+ sleepDeepMinutes: sleep.deepSleepDurationInSeconds
218
+ ? Math.floor(sleep.deepSleepDurationInSeconds / 60)
219
+ : undefined,
220
+ sleepLightMinutes: sleep.lightSleepDurationInSeconds
221
+ ? Math.floor(sleep.lightSleepDurationInSeconds / 60)
222
+ : undefined,
223
+ sleepRemMinutes: sleep.remSleepInSeconds ? Math.floor(sleep.remSleepInSeconds / 60) : undefined,
224
+ sleepAwakeMinutes: sleep.awakeDurationInSeconds
225
+ ? Math.floor(sleep.awakeDurationInSeconds / 60)
226
+ : undefined,
227
+ sleepEfficiencyScore: sleep.overallSleepScore?.value,
228
+ sleepStages,
229
+ };
230
+ }
231
+ export function normalizeSleepSummary(sleep) {
232
+ return {
233
+ date: isoDateFromTimestamp(sleep.startTimeInSeconds * 1000),
234
+ category: "sleep",
235
+ sleepDurationMinutes: Math.floor(sleep.durationInSeconds / 60),
236
+ sleepEfficiency: sleep.overallSleepScore?.value,
237
+ deepSleepMinutes: sleep.deepSleepDurationInSeconds
238
+ ? Math.floor(sleep.deepSleepDurationInSeconds / 60)
239
+ : undefined,
240
+ lightSleepMinutes: sleep.lightSleepDurationInSeconds
241
+ ? Math.floor(sleep.lightSleepDurationInSeconds / 60)
242
+ : undefined,
243
+ remSleepMinutes: sleep.remSleepInSeconds ? Math.floor(sleep.remSleepInSeconds / 60) : undefined,
244
+ awakeDuringMinutes: sleep.awakeDurationInSeconds
245
+ ? Math.floor(sleep.awakeDurationInSeconds / 60)
246
+ : undefined,
247
+ };
248
+ }
249
+ export function normalizeDaily(daily) {
250
+ const activeMinutes = ((daily.moderateIntensityDurationInSeconds ?? 0) +
251
+ (daily.vigorousIntensityDurationInSeconds ?? 0)) /
252
+ 60 || undefined;
253
+ // Parse heart rate time-offset samples into absolute timestamps
254
+ let heartRateSamples;
255
+ if (daily.timeOffsetHeartRateSamples) {
256
+ const baseMs = daily.startTimeInSeconds * 1000;
257
+ heartRateSamples = Object.entries(daily.timeOffsetHeartRateSamples).map(([offsetStr, bpm]) => ({
258
+ timestamp: baseMs + Number(offsetStr) * 1000,
259
+ value: bpm,
260
+ }));
261
+ }
262
+ const totalCalories = daily.activeKilocalories != null && daily.bmrKilocalories != null
263
+ ? daily.activeKilocalories + daily.bmrKilocalories
264
+ : daily.activeKilocalories;
265
+ return {
266
+ userId: daily.userId,
267
+ date: daily.calendarDate ?? new Date(daily.startTimeInSeconds * 1000).toISOString().split("T")[0],
268
+ totalSteps: daily.steps,
269
+ totalCalories,
270
+ activeCalories: daily.activeKilocalories,
271
+ totalDistance: daily.distanceInMeters,
272
+ floorsClimbed: daily.floorsClimbed,
273
+ avgHeartRate: daily.averageHeartRateInBeatsPerMinute,
274
+ maxHeartRate: daily.maxHeartRateInBeatsPerMinute,
275
+ minHeartRate: daily.minHeartRateInBeatsPerMinute,
276
+ restingHeartRate: daily.restingHeartRateInBeatsPerMinute,
277
+ avgStressLevel: daily.averageStressLevel,
278
+ bodyBattery: daily.bodyBatteryChargedValue != null && daily.bodyBatteryDrainedValue != null
279
+ ? daily.bodyBatteryChargedValue - daily.bodyBatteryDrainedValue
280
+ : undefined,
281
+ activeMinutes: activeMinutes ? Math.round(activeMinutes) : undefined,
282
+ heartRateSamples,
283
+ };
284
+ }
285
+ export function normalizeDailyRecoverySummary(daily) {
286
+ const normalized = normalizeDaily(daily);
287
+ return {
288
+ date: normalized.date,
289
+ category: "recovery",
290
+ restingHeartRate: normalized.restingHeartRate,
291
+ avgStressLevel: normalized.avgStressLevel,
292
+ bodyBattery: normalized.bodyBattery,
293
+ };
294
+ }
295
+ export function normalizeEpochDataPoints(epoch) {
296
+ const recordedAt = epoch.startTimeInSeconds * 1000;
297
+ const points = [];
298
+ if (epoch.meanHeartRateInBeatsPerMinute != null) {
299
+ points.push({
300
+ seriesType: "heart_rate",
301
+ recordedAt,
302
+ value: epoch.meanHeartRateInBeatsPerMinute,
303
+ externalId: epoch.summaryId ? `${epoch.summaryId}:heart_rate` : undefined,
304
+ });
305
+ }
306
+ if (epoch.steps != null) {
307
+ points.push({
308
+ seriesType: "steps",
309
+ recordedAt,
310
+ value: epoch.steps,
311
+ externalId: epoch.summaryId ? `${epoch.summaryId}:steps` : undefined,
312
+ });
313
+ }
314
+ if (epoch.activeKilocalories != null) {
315
+ points.push({
316
+ seriesType: "energy",
317
+ recordedAt,
318
+ value: epoch.activeKilocalories,
319
+ externalId: epoch.summaryId ? `${epoch.summaryId}:energy` : undefined,
320
+ });
321
+ }
322
+ return points;
323
+ }
324
+ export function normalizeBodyCompositionDataPoints(bodyComp) {
325
+ const recordedAt = bodyComp.measurementTimeInSeconds * 1000;
326
+ const points = [];
327
+ if (bodyComp.weightInGrams != null) {
328
+ points.push({
329
+ seriesType: "weight",
330
+ recordedAt,
331
+ value: bodyComp.weightInGrams / 1000,
332
+ externalId: bodyComp.summaryId ? `${bodyComp.summaryId}:weight` : undefined,
333
+ });
334
+ }
335
+ if (bodyComp.bodyFatInPercent != null) {
336
+ points.push({
337
+ seriesType: "body_fat_percentage",
338
+ recordedAt,
339
+ value: bodyComp.bodyFatInPercent,
340
+ externalId: bodyComp.summaryId ? `${bodyComp.summaryId}:body_fat_percentage` : undefined,
341
+ });
342
+ }
343
+ if (bodyComp.bodyMassIndex != null) {
344
+ points.push({
345
+ seriesType: "body_mass_index",
346
+ recordedAt,
347
+ value: bodyComp.bodyMassIndex,
348
+ externalId: bodyComp.summaryId ? `${bodyComp.summaryId}:body_mass_index` : undefined,
349
+ });
350
+ }
351
+ if (bodyComp.muscleMassInGrams != null) {
352
+ points.push({
353
+ seriesType: "skeletal_muscle_mass",
354
+ recordedAt,
355
+ value: bodyComp.muscleMassInGrams / 1000,
356
+ externalId: bodyComp.summaryId ? `${bodyComp.summaryId}:skeletal_muscle_mass` : undefined,
357
+ });
358
+ }
359
+ return points;
360
+ }
361
+ export function normalizeBodyCompositionSummary(bodyComp) {
362
+ return {
363
+ date: isoDateFromTimestamp(bodyComp.measurementTimeInSeconds * 1000),
364
+ category: "body",
365
+ weight: bodyComp.weightInGrams != null ? bodyComp.weightInGrams / 1000 : undefined,
366
+ bodyFatPercentage: bodyComp.bodyFatInPercent,
367
+ bodyMassIndex: bodyComp.bodyMassIndex,
368
+ };
369
+ }
370
+ export function normalizeHrvDataPoints(hrv) {
371
+ if (!hrv.startTimeInSeconds) {
372
+ return [];
373
+ }
374
+ const points = [];
375
+ if (hrv.lastNightAvg != null) {
376
+ points.push({
377
+ seriesType: "heart_rate_variability_sdnn",
378
+ recordedAt: hrv.startTimeInSeconds * 1000,
379
+ value: hrv.lastNightAvg,
380
+ externalId: hrv.summaryId,
381
+ });
382
+ }
383
+ points.push(...buildOffsetDataPoints(hrv.hrvValues, hrv.startTimeInSeconds, "heart_rate_variability_sdnn", hrv.summaryId));
384
+ return points;
385
+ }
386
+ export function normalizeHrvSummary(hrv) {
387
+ if (hrv.lastNightAvg == null || !hrv.startTimeInSeconds) {
388
+ return null;
389
+ }
390
+ return {
391
+ date: isoDateFromCalendarDate(hrv.calendarDate, hrv.startTimeInSeconds * 1000),
392
+ category: "recovery",
393
+ hrvAvg: hrv.lastNightAvg,
394
+ };
395
+ }
396
+ export function normalizeStressDataPoints(stress) {
397
+ if (!stress.startTimeInSeconds) {
398
+ return [];
399
+ }
400
+ return [
401
+ ...buildOffsetDataPoints(stress.stressLevelValues, stress.startTimeInSeconds, "garmin_stress_level", stress.summaryId ? `${stress.summaryId}:stress` : undefined),
402
+ ...buildOffsetDataPoints(stress.bodyBatteryValues, stress.startTimeInSeconds, "garmin_body_battery", stress.summaryId ? `${stress.summaryId}:body_battery` : undefined),
403
+ ];
404
+ }
405
+ export function normalizeRespirationDataPoints(respiration) {
406
+ if (!respiration.startTimeInSeconds) {
407
+ return [];
408
+ }
409
+ const points = [];
410
+ if (respiration.avgWakingRespirationValue != null) {
411
+ points.push({
412
+ seriesType: "respiratory_rate",
413
+ recordedAt: respiration.startTimeInSeconds * 1000,
414
+ value: respiration.avgWakingRespirationValue,
415
+ externalId: respiration.summaryId,
416
+ });
417
+ }
418
+ points.push(...buildOffsetDataPoints(respiration.timeOffsetRespirationRateValues ?? respiration.timeOffsetRespirationValues, respiration.startTimeInSeconds, "respiratory_rate", respiration.summaryId));
419
+ return points;
420
+ }
421
+ export function normalizePulseOxDataPoints(pulseOx) {
422
+ if (!pulseOx.startTimeInSeconds) {
423
+ return [];
424
+ }
425
+ const points = [];
426
+ if (pulseOx.avgSpo2 != null) {
427
+ points.push({
428
+ seriesType: "oxygen_saturation",
429
+ recordedAt: pulseOx.startTimeInSeconds * 1000,
430
+ value: pulseOx.avgSpo2,
431
+ externalId: pulseOx.summaryId,
432
+ });
433
+ }
434
+ points.push(...buildOffsetDataPoints(pulseOx.timeOffsetSpo2Values, pulseOx.startTimeInSeconds, "oxygen_saturation", pulseOx.summaryId));
435
+ return points;
436
+ }
437
+ export function normalizePulseOxSummary(pulseOx) {
438
+ if (pulseOx.avgSpo2 == null || !pulseOx.startTimeInSeconds) {
439
+ return null;
440
+ }
441
+ return {
442
+ date: isoDateFromCalendarDate(pulseOx.calendarDate, pulseOx.startTimeInSeconds * 1000),
443
+ category: "recovery",
444
+ spo2Avg: pulseOx.avgSpo2,
445
+ };
446
+ }
447
+ export function normalizeBloodPressureDataPoints(bloodPressure) {
448
+ const measurementSeconds = bloodPressure.measurementTimestampGMT ?? bloodPressure.startTimeInSeconds;
449
+ if (!measurementSeconds) {
450
+ return [];
451
+ }
452
+ const recordedAt = measurementSeconds * 1000;
453
+ const points = [];
454
+ if (bloodPressure.systolic != null) {
455
+ points.push({
456
+ seriesType: "blood_pressure_systolic",
457
+ recordedAt,
458
+ value: bloodPressure.systolic,
459
+ externalId: bloodPressure.summaryId ? `${bloodPressure.summaryId}:systolic` : undefined,
460
+ });
461
+ }
462
+ if (bloodPressure.diastolic != null) {
463
+ points.push({
464
+ seriesType: "blood_pressure_diastolic",
465
+ recordedAt,
466
+ value: bloodPressure.diastolic,
467
+ externalId: bloodPressure.summaryId ? `${bloodPressure.summaryId}:diastolic` : undefined,
468
+ });
469
+ }
470
+ return points;
471
+ }
472
+ export function normalizeUserMetricsDataPoints(userMetrics) {
473
+ const recordedAt = calendarDateToMiddayTimestamp(userMetrics.calendarDate);
474
+ if (recordedAt === null) {
475
+ return [];
476
+ }
477
+ const points = [];
478
+ if (userMetrics.vo2Max != null) {
479
+ points.push({
480
+ seriesType: "vo2_max",
481
+ recordedAt,
482
+ value: userMetrics.vo2Max,
483
+ externalId: userMetrics.summaryId ? `${userMetrics.summaryId}:vo2_max` : undefined,
484
+ });
485
+ }
486
+ if (userMetrics.fitnessAge != null) {
487
+ points.push({
488
+ seriesType: "garmin_fitness_age",
489
+ recordedAt,
490
+ value: userMetrics.fitnessAge,
491
+ externalId: userMetrics.summaryId ? `${userMetrics.summaryId}:fitness_age` : undefined,
492
+ });
493
+ }
494
+ return points;
495
+ }
496
+ export function normalizeSkinTemperatureDataPoints(skinTemp) {
497
+ if (!skinTemp.startTimeInSeconds || skinTemp.skinTemperature == null) {
498
+ return [];
499
+ }
500
+ return [
501
+ {
502
+ seriesType: "skin_temperature",
503
+ recordedAt: skinTemp.startTimeInSeconds * 1000,
504
+ value: skinTemp.skinTemperature,
505
+ externalId: skinTemp.summaryId,
506
+ },
507
+ ];
508
+ }
509
+ export function normalizeHealthSnapshotDataPoints(snapshot) {
510
+ if (!snapshot.startTimeInSeconds) {
511
+ return [];
512
+ }
513
+ const recordedAt = snapshot.startTimeInSeconds * 1000;
514
+ const points = [];
515
+ if (snapshot.heartRate != null) {
516
+ points.push({
517
+ seriesType: "heart_rate",
518
+ recordedAt,
519
+ value: snapshot.heartRate,
520
+ externalId: snapshot.summaryId ? `${snapshot.summaryId}:heart_rate` : undefined,
521
+ });
522
+ }
523
+ if (snapshot.hrv != null) {
524
+ points.push({
525
+ seriesType: "heart_rate_variability_sdnn",
526
+ recordedAt,
527
+ value: snapshot.hrv,
528
+ externalId: snapshot.summaryId ? `${snapshot.summaryId}:hrv` : undefined,
529
+ });
530
+ }
531
+ if (snapshot.stress != null) {
532
+ points.push({
533
+ seriesType: "garmin_stress_level",
534
+ recordedAt,
535
+ value: snapshot.stress,
536
+ externalId: snapshot.summaryId ? `${snapshot.summaryId}:stress` : undefined,
537
+ });
538
+ }
539
+ if (snapshot.spo2 != null) {
540
+ points.push({
541
+ seriesType: "oxygen_saturation",
542
+ recordedAt,
543
+ value: snapshot.spo2,
544
+ externalId: snapshot.summaryId ? `${snapshot.summaryId}:spo2` : undefined,
545
+ });
546
+ }
547
+ if (snapshot.respiration != null) {
548
+ points.push({
549
+ seriesType: "respiratory_rate",
550
+ recordedAt,
551
+ value: snapshot.respiration,
552
+ externalId: snapshot.summaryId ? `${snapshot.summaryId}:respiration` : undefined,
553
+ });
554
+ }
555
+ return points;
556
+ }
557
+ export function normalizeMoveIQ(moveIQ) {
558
+ const startMs = moveIQ.startTimeInSeconds * 1000;
559
+ const durationSeconds = moveIQ.durationInSeconds ?? 0;
560
+ const endMs = startMs + durationSeconds * 1000;
561
+ const type = moveIQ.activityType ? `moveiq_${moveIQ.activityType.toLowerCase()}` : "moveiq";
562
+ return {
563
+ category: "workout",
564
+ type,
565
+ sourceName: "Garmin",
566
+ durationSeconds,
567
+ startDatetime: startMs,
568
+ endDatetime: endMs,
569
+ externalId: moveIQ.summaryId
570
+ ? `garmin-moveiq-${moveIQ.summaryId}`
571
+ : `garmin-moveiq-${moveIQ.startTimeInSeconds}-${type}`,
572
+ };
573
+ }
574
+ export function normalizeMCT(mct) {
575
+ const periodStartDate = mct.periodStartDateStr ??
576
+ (mct.startTimeInSeconds
577
+ ? new Date(mct.startTimeInSeconds * 1000).toISOString().split("T")[0]
578
+ : new Date().toISOString().split("T")[0]);
579
+ return {
580
+ externalId: `garmin-mct-${mct.summaryId}`,
581
+ periodStartDate,
582
+ dayInCycle: mct.dayInCycle,
583
+ cycleLength: mct.cycleLength,
584
+ predictedCycleLength: mct.predictedCycleLength,
585
+ periodLength: mct.periodLength,
586
+ currentPhase: mct.currentPhase,
587
+ currentPhaseType: mct.currentPhaseType,
588
+ lengthOfCurrentPhase: mct.lengthOfCurrentPhase,
589
+ daysUntilNextPhase: mct.daysUntilNextPhase,
590
+ isPredictedCycle: mct.isPredictedCycle,
591
+ fertileWindowStart: mct.fertileWindowStart,
592
+ lengthOfFertileWindow: mct.lengthOfFertileWindow,
593
+ lastUpdatedAt: mct.lastUpdatedAt ? mct.lastUpdatedAt * 1000 : undefined,
594
+ isPregnant: mct.isPregnant,
595
+ pregnancyDueDate: mct.pregnancyDueDate,
596
+ pregnancyOriginalDueDate: mct.pregnancyOriginalDueDate,
597
+ pregnancyCycleStartDate: mct.pregnancyCycleStartDate,
598
+ pregnancyTitle: mct.pregnancyTitle,
599
+ numberOfBabies: mct.numberOfBabies,
600
+ };
601
+ }
602
+ // ---------------------------------------------------------------------------
603
+ // User info
604
+ // ---------------------------------------------------------------------------
605
+ export async function getGarminUserInfo(accessToken, _tokenResponse, _appUserId, _credentials) {
606
+ try {
607
+ const data = await makeAuthenticatedRequest(API_BASE, "/wellness-api/rest/user/id", accessToken);
608
+ return {
609
+ providerUserId: data.userId ?? null,
610
+ username: null,
611
+ };
612
+ }
613
+ catch {
614
+ return { providerUserId: null, username: null };
615
+ }
616
+ }
617
+ export const garminProvider = {
618
+ name: "garmin",
619
+ oauthConfig: garminOAuthConfig,
620
+ getUserInfo: getGarminUserInfo,
621
+ fetchEvents: fetchGarminWorkouts,
622
+ };
623
+ // ---------------------------------------------------------------------------
624
+ // Garmin is push-based — no fetchWorkouts (data comes via webhooks)
625
+ // ---------------------------------------------------------------------------
626
+ /**
627
+ * Garmin does not support pull-based data fetching.
628
+ * Data is pushed via webhooks. This function exists to satisfy the
629
+ * ProviderDefinition interface but returns an empty array.
630
+ *
631
+ * Use the backfill API to trigger historical data push.
632
+ */
633
+ export async function fetchGarminWorkouts(_accessToken, _startDate, _endDate, _credentials) {
634
+ // Garmin is push-only — data comes via webhooks, not pull requests.
635
+ // To get historical data, call the backfill API which triggers Garmin
636
+ // to push data to your webhook endpoints.
637
+ return [];
638
+ }
639
+ // ---------------------------------------------------------------------------
640
+ // Backfill trigger
641
+ // ---------------------------------------------------------------------------
642
+ /**
643
+ * Trigger a Garmin backfill request. Garmin will asynchronously push
644
+ * historical data to your webhook endpoints.
645
+ */
646
+ export async function triggerBackfill(accessToken, dataType, startTimeSeconds, endTimeSeconds) {
647
+ const validTypes = [
648
+ "activities",
649
+ "activityDetails",
650
+ "dailies",
651
+ "epochs",
652
+ "sleeps",
653
+ "bodyComps",
654
+ "hrv",
655
+ "stressDetails",
656
+ "respiration",
657
+ "pulseOx",
658
+ "bloodPressures",
659
+ "userMetrics",
660
+ "skinTemp",
661
+ "healthSnapshot",
662
+ "moveiq",
663
+ "mct",
664
+ ];
665
+ if (!validTypes.includes(dataType)) {
666
+ throw new Error(`Invalid backfill data type: ${dataType}`);
667
+ }
668
+ await makeAuthenticatedRequest(API_BASE, `/wellness-api/rest/backfill/${dataType}`, accessToken, {
669
+ params: {
670
+ summaryStartTimeInSeconds: String(startTimeSeconds),
671
+ summaryEndTimeInSeconds: String(endTimeSeconds),
672
+ },
673
+ });
674
+ }
675
+ //# sourceMappingURL=garmin.js.map