@contractspec/integration.providers-impls 3.8.9 → 3.8.11

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 (147) hide show
  1. package/dist/analytics.js +1 -2
  2. package/dist/calendar.js +1 -2
  3. package/dist/database.js +1 -2
  4. package/dist/email.js +1 -2
  5. package/dist/embedding.js +1 -2
  6. package/dist/health.js +1 -2
  7. package/dist/impls/async-event-queue.js +1 -48
  8. package/dist/impls/composio-fallback-resolver.js +1 -579
  9. package/dist/impls/composio-mcp.js +1 -163
  10. package/dist/impls/composio-proxies.js +1 -310
  11. package/dist/impls/composio-sdk.js +1 -77
  12. package/dist/impls/composio-types.js +1 -53
  13. package/dist/impls/elevenlabs-voice.js +1 -104
  14. package/dist/impls/fal-voice.js +1 -117
  15. package/dist/impls/fathom-meeting-recorder.js +2 -289
  16. package/dist/impls/fathom-meeting-recorder.mapper.js +1 -107
  17. package/dist/impls/fathom-meeting-recorder.utils.js +1 -74
  18. package/dist/impls/fathom-meeting-recorder.webhooks.js +1 -31
  19. package/dist/impls/fireflies-meeting-recorder.js +5 -203
  20. package/dist/impls/fireflies-meeting-recorder.queries.js +4 -14
  21. package/dist/impls/fireflies-meeting-recorder.utils.js +1 -44
  22. package/dist/impls/gcs-storage.js +1 -99
  23. package/dist/impls/gmail-inbound.js +1 -229
  24. package/dist/impls/gmail-outbound.js +25 -137
  25. package/dist/impls/google-calendar.js +1 -193
  26. package/dist/impls/gradium-voice.js +1 -95
  27. package/dist/impls/granola-meeting-recorder.js +3 -514
  28. package/dist/impls/granola-meeting-recorder.mcp.js +1 -280
  29. package/dist/impls/health/base-health-provider.js +1 -617
  30. package/dist/impls/health/hybrid-health-providers.js +1 -1089
  31. package/dist/impls/health/official-health-providers.js +1 -969
  32. package/dist/impls/health/provider-normalizers.js +1 -288
  33. package/dist/impls/health/providers.js +1 -1095
  34. package/dist/impls/health-provider-factory.js +1 -1309
  35. package/dist/impls/index.js +42 -7448
  36. package/dist/impls/jira.js +1 -126
  37. package/dist/impls/linear.js +1 -85
  38. package/dist/impls/messaging-github.js +1 -111
  39. package/dist/impls/messaging-slack.js +1 -81
  40. package/dist/impls/messaging-telegram.js +1 -48
  41. package/dist/impls/messaging-whatsapp-meta.js +1 -53
  42. package/dist/impls/messaging-whatsapp-twilio.js +1 -83
  43. package/dist/impls/mistral-conversational.js +2 -477
  44. package/dist/impls/mistral-conversational.session.js +2 -207
  45. package/dist/impls/mistral-embedding.js +1 -45
  46. package/dist/impls/mistral-llm.js +1 -271
  47. package/dist/impls/mistral-stt.js +1 -168
  48. package/dist/impls/notion.js +1 -162
  49. package/dist/impls/posthog-reader.js +1 -161
  50. package/dist/impls/posthog-utils.js +1 -40
  51. package/dist/impls/posthog.js +1 -324
  52. package/dist/impls/postmark-email.js +1 -62
  53. package/dist/impls/powens-client.js +1 -197
  54. package/dist/impls/powens-openbanking.js +1 -428
  55. package/dist/impls/provider-factory.js +18 -6268
  56. package/dist/impls/qdrant-vector.js +1 -80
  57. package/dist/impls/stripe-payments.js +1 -230
  58. package/dist/impls/supabase-psql.js +1 -152
  59. package/dist/impls/supabase-vector.js +9 -298
  60. package/dist/impls/tldv-meeting-recorder.js +2 -147
  61. package/dist/impls/twilio-sms.js +1 -67
  62. package/dist/index.js +42 -7495
  63. package/dist/llm.js +1 -2
  64. package/dist/meeting-recorder.js +1 -2
  65. package/dist/messaging.js +1 -2
  66. package/dist/node/analytics.js +1 -2
  67. package/dist/node/calendar.js +1 -2
  68. package/dist/node/database.js +1 -2
  69. package/dist/node/email.js +1 -2
  70. package/dist/node/embedding.js +1 -2
  71. package/dist/node/health.js +1 -2
  72. package/dist/node/impls/async-event-queue.js +1 -49
  73. package/dist/node/impls/composio-fallback-resolver.js +1 -580
  74. package/dist/node/impls/composio-mcp.js +1 -164
  75. package/dist/node/impls/composio-proxies.js +1 -311
  76. package/dist/node/impls/composio-sdk.js +1 -78
  77. package/dist/node/impls/composio-types.js +1 -54
  78. package/dist/node/impls/elevenlabs-voice.js +1 -105
  79. package/dist/node/impls/fal-voice.js +1 -118
  80. package/dist/node/impls/fathom-meeting-recorder.js +2 -290
  81. package/dist/node/impls/fathom-meeting-recorder.mapper.js +1 -108
  82. package/dist/node/impls/fathom-meeting-recorder.utils.js +1 -75
  83. package/dist/node/impls/fathom-meeting-recorder.webhooks.js +1 -32
  84. package/dist/node/impls/fireflies-meeting-recorder.js +5 -204
  85. package/dist/node/impls/fireflies-meeting-recorder.queries.js +4 -15
  86. package/dist/node/impls/fireflies-meeting-recorder.utils.js +1 -45
  87. package/dist/node/impls/gcs-storage.js +1 -100
  88. package/dist/node/impls/gmail-inbound.js +1 -230
  89. package/dist/node/impls/gmail-outbound.js +25 -138
  90. package/dist/node/impls/google-calendar.js +1 -194
  91. package/dist/node/impls/gradium-voice.js +1 -96
  92. package/dist/node/impls/granola-meeting-recorder.js +3 -515
  93. package/dist/node/impls/granola-meeting-recorder.mcp.js +1 -281
  94. package/dist/node/impls/health/base-health-provider.js +1 -618
  95. package/dist/node/impls/health/hybrid-health-providers.js +1 -1090
  96. package/dist/node/impls/health/official-health-providers.js +1 -970
  97. package/dist/node/impls/health/provider-normalizers.js +1 -289
  98. package/dist/node/impls/health/providers.js +1 -1096
  99. package/dist/node/impls/health-provider-factory.js +1 -1310
  100. package/dist/node/impls/index.js +42 -7449
  101. package/dist/node/impls/jira.js +1 -127
  102. package/dist/node/impls/linear.js +1 -86
  103. package/dist/node/impls/messaging-github.js +1 -112
  104. package/dist/node/impls/messaging-slack.js +1 -82
  105. package/dist/node/impls/messaging-telegram.js +1 -49
  106. package/dist/node/impls/messaging-whatsapp-meta.js +1 -54
  107. package/dist/node/impls/messaging-whatsapp-twilio.js +1 -84
  108. package/dist/node/impls/mistral-conversational.js +2 -478
  109. package/dist/node/impls/mistral-conversational.session.js +2 -208
  110. package/dist/node/impls/mistral-embedding.js +1 -46
  111. package/dist/node/impls/mistral-llm.js +1 -272
  112. package/dist/node/impls/mistral-stt.js +1 -169
  113. package/dist/node/impls/notion.js +1 -163
  114. package/dist/node/impls/posthog-reader.js +1 -162
  115. package/dist/node/impls/posthog-utils.js +1 -41
  116. package/dist/node/impls/posthog.js +1 -325
  117. package/dist/node/impls/postmark-email.js +1 -63
  118. package/dist/node/impls/powens-client.js +1 -198
  119. package/dist/node/impls/powens-openbanking.js +1 -429
  120. package/dist/node/impls/provider-factory.js +18 -6269
  121. package/dist/node/impls/qdrant-vector.js +1 -81
  122. package/dist/node/impls/stripe-payments.js +1 -231
  123. package/dist/node/impls/supabase-psql.js +1 -153
  124. package/dist/node/impls/supabase-vector.js +9 -299
  125. package/dist/node/impls/tldv-meeting-recorder.js +2 -148
  126. package/dist/node/impls/twilio-sms.js +1 -68
  127. package/dist/node/index.js +42 -7496
  128. package/dist/node/llm.js +1 -2
  129. package/dist/node/meeting-recorder.js +1 -2
  130. package/dist/node/messaging.js +1 -2
  131. package/dist/node/openbanking.js +1 -2
  132. package/dist/node/payments.js +1 -2
  133. package/dist/node/project-management.js +1 -2
  134. package/dist/node/secrets/provider.js +1 -14
  135. package/dist/node/sms.js +1 -2
  136. package/dist/node/storage.js +1 -2
  137. package/dist/node/vector-store.js +1 -2
  138. package/dist/node/voice.js +1 -2
  139. package/dist/openbanking.js +1 -2
  140. package/dist/payments.js +1 -2
  141. package/dist/project-management.js +1 -2
  142. package/dist/secrets/provider.js +1 -13
  143. package/dist/sms.js +1 -2
  144. package/dist/storage.js +1 -2
  145. package/dist/vector-store.js +1 -2
  146. package/dist/voice.js +1 -2
  147. package/package.json +16 -16
@@ -1,1310 +1,2 @@
1
1
  // @bun
2
- var __require = import.meta.require;
3
-
4
- // src/impls/health/provider-normalizers.ts
5
- var DEFAULT_LIST_KEYS = [
6
- "items",
7
- "data",
8
- "records",
9
- "activities",
10
- "workouts",
11
- "sleep",
12
- "biometrics",
13
- "nutrition"
14
- ];
15
- function asRecord(value) {
16
- if (!value || typeof value !== "object" || Array.isArray(value)) {
17
- return;
18
- }
19
- return value;
20
- }
21
- function asArray(value) {
22
- return Array.isArray(value) ? value : undefined;
23
- }
24
- function readString(record, keys) {
25
- if (!record)
26
- return;
27
- for (const key of keys) {
28
- const value = record[key];
29
- if (typeof value === "string" && value.trim().length > 0) {
30
- return value;
31
- }
32
- }
33
- return;
34
- }
35
- function readNumber(record, keys) {
36
- if (!record)
37
- return;
38
- for (const key of keys) {
39
- const value = record[key];
40
- if (typeof value === "number" && Number.isFinite(value)) {
41
- return value;
42
- }
43
- if (typeof value === "string" && value.trim().length > 0) {
44
- const parsed = Number(value);
45
- if (Number.isFinite(parsed)) {
46
- return parsed;
47
- }
48
- }
49
- }
50
- return;
51
- }
52
- function readBoolean(record, keys) {
53
- if (!record)
54
- return;
55
- for (const key of keys) {
56
- const value = record[key];
57
- if (typeof value === "boolean") {
58
- return value;
59
- }
60
- }
61
- return;
62
- }
63
- function extractList(payload, listKeys = DEFAULT_LIST_KEYS) {
64
- const root = asRecord(payload);
65
- if (!root) {
66
- return asArray(payload)?.map((item) => asRecord(item)).filter((item) => Boolean(item)) ?? [];
67
- }
68
- for (const key of listKeys) {
69
- const arrayValue = asArray(root[key]);
70
- if (!arrayValue)
71
- continue;
72
- return arrayValue.map((item) => asRecord(item)).filter((item) => Boolean(item));
73
- }
74
- return [];
75
- }
76
- function extractPagination(payload) {
77
- const root = asRecord(payload);
78
- const nestedPagination = asRecord(root?.pagination);
79
- const nextCursor = readString(nestedPagination, ["nextCursor", "next_cursor"]) ?? readString(root, [
80
- "nextCursor",
81
- "next_cursor",
82
- "cursor",
83
- "next_page_token"
84
- ]);
85
- const hasMore = readBoolean(nestedPagination, ["hasMore", "has_more"]) ?? readBoolean(root, ["hasMore", "has_more"]);
86
- return {
87
- nextCursor,
88
- hasMore: hasMore ?? Boolean(nextCursor)
89
- };
90
- }
91
- function toHealthActivity(item, context, fallbackType = "activity") {
92
- const externalId = readString(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:${fallbackType}`;
93
- const id = readString(item, ["id", "uuid", "workout_id"]) ?? `${context.providerKey}:activity:${externalId}`;
94
- return {
95
- id,
96
- externalId,
97
- tenantId: context.tenantId,
98
- connectionId: context.connectionId ?? "unknown",
99
- userId: readString(item, ["user_id", "userId", "athlete_id"]),
100
- providerKey: context.providerKey,
101
- activityType: readString(item, ["activity_type", "type", "sport_type", "sport"]) ?? fallbackType,
102
- startedAt: readIsoDate(item, [
103
- "started_at",
104
- "start_time",
105
- "start_date",
106
- "created_at"
107
- ]),
108
- endedAt: readIsoDate(item, ["ended_at", "end_time"]),
109
- durationSeconds: readNumber(item, [
110
- "duration_seconds",
111
- "duration",
112
- "elapsed_time"
113
- ]),
114
- distanceMeters: readNumber(item, ["distance_meters", "distance"]),
115
- caloriesKcal: readNumber(item, [
116
- "calories_kcal",
117
- "calories",
118
- "active_kilocalories"
119
- ]),
120
- steps: readNumber(item, ["steps"])?.valueOf(),
121
- metadata: item
122
- };
123
- }
124
- function toHealthWorkout(item, context, fallbackType = "workout") {
125
- const activity = toHealthActivity(item, context, fallbackType);
126
- return {
127
- id: activity.id,
128
- externalId: activity.externalId,
129
- tenantId: activity.tenantId,
130
- connectionId: activity.connectionId,
131
- userId: activity.userId,
132
- providerKey: activity.providerKey,
133
- workoutType: readString(item, [
134
- "workout_type",
135
- "sport_type",
136
- "type",
137
- "activity_type"
138
- ]) ?? fallbackType,
139
- startedAt: activity.startedAt,
140
- endedAt: activity.endedAt,
141
- durationSeconds: activity.durationSeconds,
142
- distanceMeters: activity.distanceMeters,
143
- caloriesKcal: activity.caloriesKcal,
144
- averageHeartRateBpm: readNumber(item, [
145
- "average_heart_rate",
146
- "avg_hr",
147
- "average_heart_rate_bpm"
148
- ]),
149
- maxHeartRateBpm: readNumber(item, [
150
- "max_heart_rate",
151
- "max_hr",
152
- "max_heart_rate_bpm"
153
- ]),
154
- metadata: item
155
- };
156
- }
157
- function toHealthSleep(item, context) {
158
- const externalId = readString(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:sleep`;
159
- const id = readString(item, ["id", "uuid"]) ?? `${context.providerKey}:sleep:${externalId}`;
160
- const startedAt = readIsoDate(item, ["started_at", "start_time", "bedtime_start", "start"]) ?? new Date(0).toISOString();
161
- const endedAt = readIsoDate(item, ["ended_at", "end_time", "bedtime_end", "end"]) ?? startedAt;
162
- return {
163
- id,
164
- externalId,
165
- tenantId: context.tenantId,
166
- connectionId: context.connectionId ?? "unknown",
167
- userId: readString(item, ["user_id", "userId"]),
168
- providerKey: context.providerKey,
169
- startedAt,
170
- endedAt,
171
- durationSeconds: readNumber(item, [
172
- "duration_seconds",
173
- "duration",
174
- "total_sleep_duration"
175
- ]),
176
- deepSleepSeconds: readNumber(item, [
177
- "deep_sleep_seconds",
178
- "deep_sleep_duration"
179
- ]),
180
- lightSleepSeconds: readNumber(item, [
181
- "light_sleep_seconds",
182
- "light_sleep_duration"
183
- ]),
184
- remSleepSeconds: readNumber(item, [
185
- "rem_sleep_seconds",
186
- "rem_sleep_duration"
187
- ]),
188
- awakeSeconds: readNumber(item, ["awake_seconds", "awake_time"]),
189
- sleepScore: readNumber(item, ["sleep_score", "score"]),
190
- metadata: item
191
- };
192
- }
193
- function toHealthBiometric(item, context, metricTypeFallback = "metric") {
194
- const externalId = readString(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:biometric`;
195
- const id = readString(item, ["id", "uuid"]) ?? `${context.providerKey}:biometric:${externalId}`;
196
- return {
197
- id,
198
- externalId,
199
- tenantId: context.tenantId,
200
- connectionId: context.connectionId ?? "unknown",
201
- userId: readString(item, ["user_id", "userId"]),
202
- providerKey: context.providerKey,
203
- metricType: readString(item, ["metric_type", "metric", "type", "name"]) ?? metricTypeFallback,
204
- value: readNumber(item, ["value", "score", "measurement"]) ?? 0,
205
- unit: readString(item, ["unit"]),
206
- measuredAt: readIsoDate(item, ["measured_at", "timestamp", "created_at"]) ?? new Date().toISOString(),
207
- metadata: item
208
- };
209
- }
210
- function toHealthNutrition(item, context) {
211
- const externalId = readString(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:nutrition`;
212
- const id = readString(item, ["id", "uuid"]) ?? `${context.providerKey}:nutrition:${externalId}`;
213
- return {
214
- id,
215
- externalId,
216
- tenantId: context.tenantId,
217
- connectionId: context.connectionId ?? "unknown",
218
- userId: readString(item, ["user_id", "userId"]),
219
- providerKey: context.providerKey,
220
- loggedAt: readIsoDate(item, ["logged_at", "created_at", "date", "timestamp"]) ?? new Date().toISOString(),
221
- caloriesKcal: readNumber(item, ["calories_kcal", "calories"]),
222
- proteinGrams: readNumber(item, ["protein_grams", "protein"]),
223
- carbsGrams: readNumber(item, ["carbs_grams", "carbs"]),
224
- fatGrams: readNumber(item, ["fat_grams", "fat"]),
225
- fiberGrams: readNumber(item, ["fiber_grams", "fiber"]),
226
- hydrationMl: readNumber(item, ["hydration_ml", "water_ml", "water"]),
227
- metadata: item
228
- };
229
- }
230
- function toHealthConnectionStatus(payload, params, source) {
231
- const record = asRecord(payload);
232
- const rawStatus = readString(record, ["status", "connection_status", "health"]) ?? "healthy";
233
- return {
234
- tenantId: params.tenantId,
235
- connectionId: params.connectionId,
236
- status: rawStatus === "healthy" || rawStatus === "degraded" || rawStatus === "error" || rawStatus === "disconnected" ? rawStatus : "healthy",
237
- source,
238
- lastCheckedAt: readIsoDate(record, ["last_checked_at", "lastCheckedAt"]) ?? new Date().toISOString(),
239
- errorCode: readString(record, ["error_code", "errorCode"]),
240
- errorMessage: readString(record, ["error_message", "errorMessage"]),
241
- metadata: asRecord(record?.metadata)
242
- };
243
- }
244
- function toHealthWebhookEvent(payload, providerKey, verified) {
245
- const record = asRecord(payload);
246
- const entityType = readString(record, ["entity_type", "entityType", "type"]);
247
- const normalizedEntityType = entityType === "activity" || entityType === "workout" || entityType === "sleep" || entityType === "biometric" || entityType === "nutrition" ? entityType : undefined;
248
- return {
249
- providerKey,
250
- eventType: readString(record, ["event_type", "eventType", "event"]),
251
- externalEntityId: readString(record, [
252
- "external_entity_id",
253
- "externalEntityId",
254
- "entity_id",
255
- "entityId",
256
- "id"
257
- ]),
258
- entityType: normalizedEntityType,
259
- receivedAt: new Date().toISOString(),
260
- verified,
261
- payload,
262
- metadata: asRecord(record?.metadata)
263
- };
264
- }
265
- function readIsoDate(record, keys) {
266
- const value = readString(record, keys);
267
- if (!value)
268
- return;
269
- const parsed = new Date(value);
270
- if (Number.isNaN(parsed.getTime()))
271
- return;
272
- return parsed.toISOString();
273
- }
274
-
275
- // src/impls/health/base-health-provider.ts
276
- class HealthProviderCapabilityError extends Error {
277
- code = "NOT_SUPPORTED";
278
- constructor(message) {
279
- super(message);
280
- this.name = "HealthProviderCapabilityError";
281
- }
282
- }
283
-
284
- class BaseHealthProvider {
285
- providerKey;
286
- transport;
287
- apiBaseUrl;
288
- mcpUrl;
289
- apiKey;
290
- accessToken;
291
- refreshToken;
292
- mcpAccessToken;
293
- webhookSecret;
294
- webhookSignatureHeader;
295
- route;
296
- aggregatorKey;
297
- oauth;
298
- fetchFn;
299
- mcpRequestId = 0;
300
- constructor(options) {
301
- this.providerKey = options.providerKey;
302
- this.transport = options.transport;
303
- this.apiBaseUrl = options.apiBaseUrl;
304
- this.mcpUrl = options.mcpUrl;
305
- this.apiKey = options.apiKey;
306
- this.accessToken = options.accessToken;
307
- this.refreshToken = options.oauth?.refreshToken;
308
- this.mcpAccessToken = options.mcpAccessToken;
309
- this.webhookSecret = options.webhookSecret;
310
- this.webhookSignatureHeader = options.webhookSignatureHeader ?? "x-webhook-signature";
311
- this.route = options.route ?? "primary";
312
- this.aggregatorKey = options.aggregatorKey;
313
- this.oauth = options.oauth ?? {};
314
- this.fetchFn = options.fetchFn ?? fetch;
315
- }
316
- async listActivities(_params) {
317
- throw this.unsupported("activities");
318
- }
319
- async listWorkouts(_params) {
320
- throw this.unsupported("workouts");
321
- }
322
- async listSleep(_params) {
323
- throw this.unsupported("sleep");
324
- }
325
- async listBiometrics(_params) {
326
- throw this.unsupported("biometrics");
327
- }
328
- async listNutrition(_params) {
329
- throw this.unsupported("nutrition");
330
- }
331
- async getConnectionStatus(params) {
332
- return this.fetchConnectionStatus(params, {
333
- mcpTool: `${this.providerSlug()}_connection_status`
334
- });
335
- }
336
- async syncActivities(params) {
337
- return this.syncFromList(() => this.listActivities(params));
338
- }
339
- async syncWorkouts(params) {
340
- return this.syncFromList(() => this.listWorkouts(params));
341
- }
342
- async syncSleep(params) {
343
- return this.syncFromList(() => this.listSleep(params));
344
- }
345
- async syncBiometrics(params) {
346
- return this.syncFromList(() => this.listBiometrics(params));
347
- }
348
- async syncNutrition(params) {
349
- return this.syncFromList(() => this.listNutrition(params));
350
- }
351
- async parseWebhook(request) {
352
- const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
353
- const verified = await this.verifyWebhook(request);
354
- return toHealthWebhookEvent(payload, this.providerKey, verified);
355
- }
356
- async verifyWebhook(request) {
357
- if (!this.webhookSecret)
358
- return true;
359
- const signature = readHeader(request.headers, this.webhookSignatureHeader);
360
- return signature === this.webhookSecret;
361
- }
362
- async fetchActivities(params, config) {
363
- const response = await this.fetchList(params, config);
364
- return {
365
- activities: response.items,
366
- nextCursor: response.nextCursor,
367
- hasMore: response.hasMore,
368
- source: this.currentSource()
369
- };
370
- }
371
- async fetchWorkouts(params, config) {
372
- const response = await this.fetchList(params, config);
373
- return {
374
- workouts: response.items,
375
- nextCursor: response.nextCursor,
376
- hasMore: response.hasMore,
377
- source: this.currentSource()
378
- };
379
- }
380
- async fetchSleep(params, config) {
381
- const response = await this.fetchList(params, config);
382
- return {
383
- sleep: response.items,
384
- nextCursor: response.nextCursor,
385
- hasMore: response.hasMore,
386
- source: this.currentSource()
387
- };
388
- }
389
- async fetchBiometrics(params, config) {
390
- const response = await this.fetchList(params, config);
391
- return {
392
- biometrics: response.items,
393
- nextCursor: response.nextCursor,
394
- hasMore: response.hasMore,
395
- source: this.currentSource()
396
- };
397
- }
398
- async fetchNutrition(params, config) {
399
- const response = await this.fetchList(params, config);
400
- return {
401
- nutrition: response.items,
402
- nextCursor: response.nextCursor,
403
- hasMore: response.hasMore,
404
- source: this.currentSource()
405
- };
406
- }
407
- async fetchConnectionStatus(params, config) {
408
- const payload = await this.fetchPayload(config, params);
409
- return toHealthConnectionStatus(payload, params, this.currentSource());
410
- }
411
- currentSource() {
412
- return {
413
- providerKey: this.providerKey,
414
- transport: this.transport,
415
- route: this.route,
416
- aggregatorKey: this.aggregatorKey
417
- };
418
- }
419
- providerSlug() {
420
- return this.providerKey.replace("health.", "").replace(/-/g, "_");
421
- }
422
- unsupported(capability) {
423
- return new HealthProviderCapabilityError(`${this.providerKey} does not support ${capability}`);
424
- }
425
- async syncFromList(executor) {
426
- const result = await executor();
427
- const records = countResultRecords(result);
428
- return {
429
- synced: records,
430
- failed: 0,
431
- nextCursor: undefined,
432
- source: result.source
433
- };
434
- }
435
- async fetchList(params, config) {
436
- const payload = await this.fetchPayload(config, params);
437
- const items = extractList(payload, config.listKeys).map((item) => config.mapItem(item, params)).filter((item) => Boolean(item));
438
- const pagination = extractPagination(payload);
439
- return {
440
- items,
441
- nextCursor: pagination.nextCursor,
442
- hasMore: pagination.hasMore
443
- };
444
- }
445
- async fetchPayload(config, params) {
446
- const method = config.method ?? "GET";
447
- const query = config.buildQuery?.(params);
448
- const body = config.buildBody?.(params);
449
- if (this.isMcpTransport()) {
450
- return this.callMcpTool(config.mcpTool, {
451
- ...query ?? {},
452
- ...body ?? {}
453
- });
454
- }
455
- if (!config.apiPath || !this.apiBaseUrl) {
456
- throw new Error(`${this.providerKey} transport is missing an API path.`);
457
- }
458
- if (method === "POST") {
459
- return this.requestApi(config.apiPath, "POST", undefined, body);
460
- }
461
- return this.requestApi(config.apiPath, "GET", query, undefined);
462
- }
463
- isMcpTransport() {
464
- return this.transport.endsWith("mcp") || this.transport === "unofficial";
465
- }
466
- async requestApi(path, method, query, body) {
467
- const url = new URL(path, ensureTrailingSlash(this.apiBaseUrl ?? ""));
468
- if (query) {
469
- for (const [key, value] of Object.entries(query)) {
470
- if (value == null)
471
- continue;
472
- if (Array.isArray(value)) {
473
- value.forEach((entry) => {
474
- if (entry != null)
475
- url.searchParams.append(key, String(entry));
476
- });
477
- continue;
478
- }
479
- url.searchParams.set(key, String(value));
480
- }
481
- }
482
- const response = await this.fetchFn(url, {
483
- method,
484
- headers: this.authorizationHeaders(),
485
- body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
486
- });
487
- if (response.status === 401 && await this.refreshAccessToken()) {
488
- const retryResponse = await this.fetchFn(url, {
489
- method,
490
- headers: this.authorizationHeaders(),
491
- body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
492
- });
493
- return this.readResponsePayload(retryResponse, path);
494
- }
495
- return this.readResponsePayload(response, path);
496
- }
497
- async callMcpTool(toolName, args) {
498
- if (!this.mcpUrl) {
499
- throw new Error(`${this.providerKey} MCP URL is not configured.`);
500
- }
501
- const response = await this.fetchFn(this.mcpUrl, {
502
- method: "POST",
503
- headers: {
504
- "Content-Type": "application/json",
505
- ...this.mcpAccessToken ? { Authorization: `Bearer ${this.mcpAccessToken}` } : {}
506
- },
507
- body: JSON.stringify({
508
- jsonrpc: "2.0",
509
- id: ++this.mcpRequestId,
510
- method: "tools/call",
511
- params: {
512
- name: toolName,
513
- arguments: args
514
- }
515
- })
516
- });
517
- const payload = await this.readResponsePayload(response, toolName);
518
- const rpcEnvelope = asRecord(payload);
519
- if (!rpcEnvelope)
520
- return payload;
521
- const rpcResult = asRecord(rpcEnvelope.result);
522
- if (rpcResult) {
523
- return rpcResult.structuredContent ?? rpcResult.data ?? rpcResult;
524
- }
525
- return rpcEnvelope.structuredContent ?? rpcEnvelope.data ?? rpcEnvelope;
526
- }
527
- authorizationHeaders() {
528
- const token = this.accessToken ?? this.apiKey;
529
- return {
530
- "Content-Type": "application/json",
531
- ...token ? { Authorization: `Bearer ${token}` } : {}
532
- };
533
- }
534
- async refreshAccessToken() {
535
- if (!this.oauth.tokenUrl || !this.refreshToken) {
536
- return false;
537
- }
538
- const tokenUrl = new URL(this.oauth.tokenUrl);
539
- const body = new URLSearchParams({
540
- grant_type: "refresh_token",
541
- refresh_token: this.refreshToken,
542
- ...this.oauth.clientId ? { client_id: this.oauth.clientId } : {},
543
- ...this.oauth.clientSecret ? { client_secret: this.oauth.clientSecret } : {}
544
- });
545
- const response = await this.fetchFn(tokenUrl, {
546
- method: "POST",
547
- headers: {
548
- "Content-Type": "application/x-www-form-urlencoded"
549
- },
550
- body: body.toString()
551
- });
552
- if (!response.ok) {
553
- return false;
554
- }
555
- const payload = await response.json();
556
- this.accessToken = payload.access_token;
557
- this.refreshToken = payload.refresh_token ?? this.refreshToken;
558
- if (typeof payload.expires_in === "number") {
559
- this.oauth.tokenExpiresAt = new Date(Date.now() + payload.expires_in * 1000).toISOString();
560
- }
561
- return Boolean(this.accessToken);
562
- }
563
- async readResponsePayload(response, context) {
564
- if (!response.ok) {
565
- const message = await safeReadText(response);
566
- throw new Error(`${this.providerKey} request ${context} failed (${response.status}): ${message}`);
567
- }
568
- if (response.status === 204) {
569
- return {};
570
- }
571
- return response.json();
572
- }
573
- }
574
- function readHeader(headers, key) {
575
- const target = key.toLowerCase();
576
- const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
577
- if (!entry)
578
- return;
579
- const value = entry[1];
580
- return Array.isArray(value) ? value[0] : value;
581
- }
582
- function countResultRecords(result) {
583
- const listKeys = [
584
- "activities",
585
- "workouts",
586
- "sleep",
587
- "biometrics",
588
- "nutrition"
589
- ];
590
- for (const key of listKeys) {
591
- const value = result[key];
592
- if (Array.isArray(value)) {
593
- return value.length;
594
- }
595
- }
596
- return 0;
597
- }
598
- function ensureTrailingSlash(value) {
599
- return value.endsWith("/") ? value : `${value}/`;
600
- }
601
- function safeJsonParse(raw) {
602
- try {
603
- return JSON.parse(raw);
604
- } catch {
605
- return { rawBody: raw };
606
- }
607
- }
608
- async function safeReadText(response) {
609
- try {
610
- return await response.text();
611
- } catch {
612
- return response.statusText;
613
- }
614
- }
615
-
616
- // src/impls/health/official-health-providers.ts
617
- function buildSharedQuery(params) {
618
- return {
619
- tenantId: params.tenantId,
620
- connectionId: params.connectionId,
621
- userId: params.userId,
622
- from: params.from,
623
- to: params.to,
624
- cursor: params.cursor,
625
- pageSize: params.pageSize
626
- };
627
- }
628
- function withMetricTypes(params) {
629
- return {
630
- ...buildSharedQuery(params),
631
- metricTypes: params.metricTypes
632
- };
633
- }
634
-
635
- class OpenWearablesHealthProvider extends BaseHealthProvider {
636
- upstreamProvider;
637
- constructor(options) {
638
- super({
639
- providerKey: options.providerKey ?? "health.openwearables",
640
- apiBaseUrl: options.apiBaseUrl ?? "https://api.openwearables.io",
641
- webhookSignatureHeader: "x-openwearables-signature",
642
- ...options
643
- });
644
- this.upstreamProvider = options.upstreamProvider;
645
- }
646
- async listActivities(params) {
647
- return this.fetchActivities(params, {
648
- apiPath: "/v1/activities",
649
- mcpTool: "openwearables_list_activities",
650
- buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
651
- mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
652
- });
653
- }
654
- async listWorkouts(params) {
655
- return this.fetchWorkouts(params, {
656
- apiPath: "/v1/workouts",
657
- mcpTool: "openwearables_list_workouts",
658
- buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
659
- mapItem: (item, input) => toHealthWorkout(item, this.context(input))
660
- });
661
- }
662
- async listSleep(params) {
663
- return this.fetchSleep(params, {
664
- apiPath: "/v1/sleep",
665
- mcpTool: "openwearables_list_sleep",
666
- buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
667
- mapItem: (item, input) => toHealthSleep(item, this.context(input))
668
- });
669
- }
670
- async listBiometrics(params) {
671
- return this.fetchBiometrics(params, {
672
- apiPath: "/v1/biometrics",
673
- mcpTool: "openwearables_list_biometrics",
674
- buildQuery: (input) => this.withUpstreamProvider(withMetricTypes(input)),
675
- mapItem: (item, input) => toHealthBiometric(item, this.context(input))
676
- });
677
- }
678
- async listNutrition(params) {
679
- return this.fetchNutrition(params, {
680
- apiPath: "/v1/nutrition",
681
- mcpTool: "openwearables_list_nutrition",
682
- buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
683
- mapItem: (item, input) => toHealthNutrition(item, this.context(input))
684
- });
685
- }
686
- async getConnectionStatus(params) {
687
- return this.fetchConnectionStatus(params, {
688
- apiPath: `/v1/connections/${encodeURIComponent(params.connectionId)}/status`,
689
- mcpTool: "openwearables_connection_status"
690
- });
691
- }
692
- withUpstreamProvider(query) {
693
- return {
694
- ...query,
695
- ...this.upstreamProvider ? { upstreamProvider: this.upstreamProvider } : {}
696
- };
697
- }
698
- context(params) {
699
- return {
700
- tenantId: params.tenantId,
701
- connectionId: params.connectionId,
702
- providerKey: this.providerKey
703
- };
704
- }
705
- }
706
-
707
- class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
708
- constructor(options) {
709
- super({
710
- ...options,
711
- providerKey: "health.apple-health",
712
- upstreamProvider: "apple-health"
713
- });
714
- }
715
- }
716
-
717
- class WhoopHealthProvider extends BaseHealthProvider {
718
- constructor(options) {
719
- super({
720
- providerKey: "health.whoop",
721
- apiBaseUrl: options.apiBaseUrl ?? "https://api.prod.whoop.com",
722
- webhookSignatureHeader: "x-whoop-signature",
723
- oauth: {
724
- tokenUrl: options.oauth?.tokenUrl ?? "https://api.prod.whoop.com/oauth/oauth2/token",
725
- ...options.oauth
726
- },
727
- ...options
728
- });
729
- }
730
- async listActivities(params) {
731
- return this.fetchActivities(params, {
732
- apiPath: "/v2/activity/workout",
733
- mcpTool: "whoop_list_activities",
734
- buildQuery: buildSharedQuery,
735
- mapItem: (item, input) => toHealthActivity(item, this.context(input), "workout")
736
- });
737
- }
738
- async listWorkouts(params) {
739
- return this.fetchWorkouts(params, {
740
- apiPath: "/v2/activity/workout",
741
- mcpTool: "whoop_list_workouts",
742
- buildQuery: buildSharedQuery,
743
- mapItem: (item, input) => toHealthWorkout(item, this.context(input))
744
- });
745
- }
746
- async listSleep(params) {
747
- return this.fetchSleep(params, {
748
- apiPath: "/v2/activity/sleep",
749
- mcpTool: "whoop_list_sleep",
750
- buildQuery: buildSharedQuery,
751
- mapItem: (item, input) => toHealthSleep(item, this.context(input))
752
- });
753
- }
754
- async listBiometrics(params) {
755
- return this.fetchBiometrics(params, {
756
- apiPath: "/v2/recovery",
757
- mcpTool: "whoop_list_biometrics",
758
- buildQuery: withMetricTypes,
759
- mapItem: (item, input) => toHealthBiometric(item, this.context(input), "recovery_score")
760
- });
761
- }
762
- async listNutrition(_params) {
763
- throw this.unsupported("nutrition");
764
- }
765
- async getConnectionStatus(params) {
766
- return this.fetchConnectionStatus(params, {
767
- apiPath: "/v2/user/profile/basic",
768
- mcpTool: "whoop_connection_status"
769
- });
770
- }
771
- context(params) {
772
- return {
773
- tenantId: params.tenantId,
774
- connectionId: params.connectionId,
775
- providerKey: this.providerKey
776
- };
777
- }
778
- }
779
-
780
- class OuraHealthProvider extends BaseHealthProvider {
781
- constructor(options) {
782
- super({
783
- providerKey: "health.oura",
784
- apiBaseUrl: options.apiBaseUrl ?? "https://api.ouraring.com",
785
- webhookSignatureHeader: "x-oura-signature",
786
- oauth: {
787
- tokenUrl: options.oauth?.tokenUrl ?? "https://api.ouraring.com/oauth/token",
788
- ...options.oauth
789
- },
790
- ...options
791
- });
792
- }
793
- async listActivities(params) {
794
- return this.fetchActivities(params, {
795
- apiPath: "/v2/usercollection/daily_activity",
796
- mcpTool: "oura_list_activities",
797
- buildQuery: buildSharedQuery,
798
- mapItem: (item, input) => toHealthActivity(item, this.context(input))
799
- });
800
- }
801
- async listWorkouts(params) {
802
- return this.fetchWorkouts(params, {
803
- apiPath: "/v2/usercollection/workout",
804
- mcpTool: "oura_list_workouts",
805
- buildQuery: buildSharedQuery,
806
- mapItem: (item, input) => toHealthWorkout(item, this.context(input))
807
- });
808
- }
809
- async listSleep(params) {
810
- return this.fetchSleep(params, {
811
- apiPath: "/v2/usercollection/sleep",
812
- mcpTool: "oura_list_sleep",
813
- buildQuery: buildSharedQuery,
814
- mapItem: (item, input) => toHealthSleep(item, this.context(input))
815
- });
816
- }
817
- async listBiometrics(params) {
818
- return this.fetchBiometrics(params, {
819
- apiPath: "/v2/usercollection/daily_readiness",
820
- mcpTool: "oura_list_biometrics",
821
- buildQuery: withMetricTypes,
822
- mapItem: (item, input) => toHealthBiometric(item, this.context(input), "readiness_score")
823
- });
824
- }
825
- async listNutrition(_params) {
826
- throw this.unsupported("nutrition");
827
- }
828
- async getConnectionStatus(params) {
829
- return this.fetchConnectionStatus(params, {
830
- apiPath: "/v2/usercollection/personal_info",
831
- mcpTool: "oura_connection_status"
832
- });
833
- }
834
- context(params) {
835
- return {
836
- tenantId: params.tenantId,
837
- connectionId: params.connectionId,
838
- providerKey: this.providerKey
839
- };
840
- }
841
- }
842
-
843
- class StravaHealthProvider extends BaseHealthProvider {
844
- constructor(options) {
845
- super({
846
- providerKey: "health.strava",
847
- apiBaseUrl: options.apiBaseUrl ?? "https://www.strava.com",
848
- webhookSignatureHeader: "x-strava-signature",
849
- oauth: {
850
- tokenUrl: options.oauth?.tokenUrl ?? "https://www.strava.com/oauth/token",
851
- ...options.oauth
852
- },
853
- ...options
854
- });
855
- }
856
- async listActivities(params) {
857
- return this.fetchActivities(params, {
858
- apiPath: "/api/v3/athlete/activities",
859
- mcpTool: "strava_list_activities",
860
- buildQuery: buildSharedQuery,
861
- mapItem: (item, input) => toHealthActivity(item, this.context(input))
862
- });
863
- }
864
- async listWorkouts(params) {
865
- return this.fetchWorkouts(params, {
866
- apiPath: "/api/v3/athlete/activities",
867
- mcpTool: "strava_list_workouts",
868
- buildQuery: buildSharedQuery,
869
- mapItem: (item, input) => toHealthWorkout(item, this.context(input))
870
- });
871
- }
872
- async listSleep(_params) {
873
- throw this.unsupported("sleep");
874
- }
875
- async listBiometrics(_params) {
876
- throw this.unsupported("biometrics");
877
- }
878
- async listNutrition(_params) {
879
- throw this.unsupported("nutrition");
880
- }
881
- async getConnectionStatus(params) {
882
- return this.fetchConnectionStatus(params, {
883
- apiPath: "/api/v3/athlete",
884
- mcpTool: "strava_connection_status"
885
- });
886
- }
887
- context(params) {
888
- return {
889
- tenantId: params.tenantId,
890
- connectionId: params.connectionId,
891
- providerKey: this.providerKey
892
- };
893
- }
894
- }
895
-
896
- class FitbitHealthProvider extends BaseHealthProvider {
897
- constructor(options) {
898
- super({
899
- providerKey: "health.fitbit",
900
- apiBaseUrl: options.apiBaseUrl ?? "https://api.fitbit.com",
901
- webhookSignatureHeader: "x-fitbit-signature",
902
- oauth: {
903
- tokenUrl: options.oauth?.tokenUrl ?? "https://api.fitbit.com/oauth2/token",
904
- ...options.oauth
905
- },
906
- ...options
907
- });
908
- }
909
- async listActivities(params) {
910
- return this.fetchActivities(params, {
911
- apiPath: "/1/user/-/activities/list.json",
912
- mcpTool: "fitbit_list_activities",
913
- buildQuery: buildSharedQuery,
914
- mapItem: (item, input) => toHealthActivity(item, this.context(input))
915
- });
916
- }
917
- async listWorkouts(params) {
918
- return this.fetchWorkouts(params, {
919
- apiPath: "/1/user/-/activities/list.json",
920
- mcpTool: "fitbit_list_workouts",
921
- buildQuery: buildSharedQuery,
922
- mapItem: (item, input) => toHealthWorkout(item, this.context(input))
923
- });
924
- }
925
- async listSleep(params) {
926
- return this.fetchSleep(params, {
927
- apiPath: "/1.2/user/-/sleep/list.json",
928
- mcpTool: "fitbit_list_sleep",
929
- buildQuery: buildSharedQuery,
930
- mapItem: (item, input) => toHealthSleep(item, this.context(input))
931
- });
932
- }
933
- async listBiometrics(params) {
934
- return this.fetchBiometrics(params, {
935
- apiPath: "/1/user/-/body/log/weight/date/today/1m.json",
936
- mcpTool: "fitbit_list_biometrics",
937
- buildQuery: withMetricTypes,
938
- mapItem: (item, input) => toHealthBiometric(item, this.context(input), "weight")
939
- });
940
- }
941
- async listNutrition(params) {
942
- return this.fetchNutrition(params, {
943
- apiPath: "/1/user/-/foods/log/date/today.json",
944
- mcpTool: "fitbit_list_nutrition",
945
- buildQuery: buildSharedQuery,
946
- mapItem: (item, input) => toHealthNutrition(item, this.context(input))
947
- });
948
- }
949
- async getConnectionStatus(params) {
950
- return this.fetchConnectionStatus(params, {
951
- apiPath: "/1/user/-/profile.json",
952
- mcpTool: "fitbit_connection_status"
953
- });
954
- }
955
- context(params) {
956
- return {
957
- tenantId: params.tenantId,
958
- connectionId: params.connectionId,
959
- providerKey: this.providerKey
960
- };
961
- }
962
- }
963
-
964
- // src/impls/health/hybrid-health-providers.ts
965
- var LIMITED_PROVIDER_SLUG = {
966
- "health.garmin": "garmin",
967
- "health.myfitnesspal": "myfitnesspal",
968
- "health.eightsleep": "eightsleep",
969
- "health.peloton": "peloton"
970
- };
971
- function buildSharedQuery2(params) {
972
- return {
973
- tenantId: params.tenantId,
974
- connectionId: params.connectionId,
975
- userId: params.userId,
976
- from: params.from,
977
- to: params.to,
978
- cursor: params.cursor,
979
- pageSize: params.pageSize
980
- };
981
- }
982
-
983
- class GarminHealthProvider extends OpenWearablesHealthProvider {
984
- constructor(options) {
985
- super({
986
- ...options,
987
- providerKey: "health.garmin",
988
- upstreamProvider: "garmin"
989
- });
990
- }
991
- }
992
-
993
- class MyFitnessPalHealthProvider extends OpenWearablesHealthProvider {
994
- constructor(options) {
995
- super({
996
- ...options,
997
- providerKey: "health.myfitnesspal",
998
- upstreamProvider: "myfitnesspal"
999
- });
1000
- }
1001
- }
1002
-
1003
- class EightSleepHealthProvider extends OpenWearablesHealthProvider {
1004
- constructor(options) {
1005
- super({
1006
- ...options,
1007
- providerKey: "health.eightsleep",
1008
- upstreamProvider: "eightsleep"
1009
- });
1010
- }
1011
- }
1012
-
1013
- class PelotonHealthProvider extends OpenWearablesHealthProvider {
1014
- constructor(options) {
1015
- super({
1016
- ...options,
1017
- providerKey: "health.peloton",
1018
- upstreamProvider: "peloton"
1019
- });
1020
- }
1021
- }
1022
-
1023
- class UnofficialHealthAutomationProvider extends BaseHealthProvider {
1024
- providerSlugValue;
1025
- constructor(options) {
1026
- super({
1027
- ...options,
1028
- providerKey: options.providerKey,
1029
- webhookSignatureHeader: "x-unofficial-signature"
1030
- });
1031
- this.providerSlugValue = LIMITED_PROVIDER_SLUG[options.providerKey];
1032
- }
1033
- async listActivities(params) {
1034
- return this.fetchActivities(params, {
1035
- mcpTool: `${this.providerSlugValue}_list_activities`,
1036
- buildQuery: buildSharedQuery2,
1037
- mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
1038
- });
1039
- }
1040
- async listWorkouts(params) {
1041
- return this.fetchWorkouts(params, {
1042
- mcpTool: `${this.providerSlugValue}_list_workouts`,
1043
- buildQuery: buildSharedQuery2,
1044
- mapItem: (item, input) => toHealthWorkout(item, this.context(input))
1045
- });
1046
- }
1047
- async listSleep(params) {
1048
- return this.fetchSleep(params, {
1049
- mcpTool: `${this.providerSlugValue}_list_sleep`,
1050
- buildQuery: buildSharedQuery2,
1051
- mapItem: (item, input) => toHealthSleep(item, this.context(input))
1052
- });
1053
- }
1054
- async listBiometrics(params) {
1055
- return this.fetchBiometrics(params, {
1056
- mcpTool: `${this.providerSlugValue}_list_biometrics`,
1057
- buildQuery: (input) => ({
1058
- ...buildSharedQuery2(input),
1059
- metricTypes: input.metricTypes
1060
- }),
1061
- mapItem: (item, input) => toHealthBiometric(item, this.context(input))
1062
- });
1063
- }
1064
- async listNutrition(params) {
1065
- return this.fetchNutrition(params, {
1066
- mcpTool: `${this.providerSlugValue}_list_nutrition`,
1067
- buildQuery: buildSharedQuery2,
1068
- mapItem: (item, input) => toHealthNutrition(item, this.context(input))
1069
- });
1070
- }
1071
- async getConnectionStatus(params) {
1072
- return this.fetchConnectionStatus(params, {
1073
- mcpTool: `${this.providerSlugValue}_connection_status`
1074
- });
1075
- }
1076
- context(params) {
1077
- return {
1078
- tenantId: params.tenantId,
1079
- connectionId: params.connectionId,
1080
- providerKey: this.providerKey
1081
- };
1082
- }
1083
- }
1084
- // src/impls/health-provider-factory.ts
1085
- import {
1086
- isUnofficialHealthProviderAllowed,
1087
- resolveHealthStrategyOrder
1088
- } from "@contractspec/integration.runtime/runtime";
1089
- var OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER = {
1090
- "health.openwearables": false,
1091
- "health.whoop": true,
1092
- "health.apple-health": false,
1093
- "health.oura": true,
1094
- "health.strava": true,
1095
- "health.garmin": false,
1096
- "health.fitbit": true,
1097
- "health.myfitnesspal": false,
1098
- "health.eightsleep": false,
1099
- "health.peloton": false
1100
- };
1101
- var UNOFFICIAL_SUPPORTED_BY_PROVIDER = {
1102
- "health.openwearables": false,
1103
- "health.whoop": false,
1104
- "health.apple-health": false,
1105
- "health.oura": false,
1106
- "health.strava": false,
1107
- "health.garmin": true,
1108
- "health.fitbit": false,
1109
- "health.myfitnesspal": true,
1110
- "health.eightsleep": true,
1111
- "health.peloton": true
1112
- };
1113
- function createHealthProviderFromContext(context, secrets) {
1114
- const providerKey = context.spec.meta.key;
1115
- const config = toFactoryConfig(context.config);
1116
- const strategyOrder = buildStrategyOrder(config);
1117
- const attemptLogs = [];
1118
- for (let index = 0;index < strategyOrder.length; index += 1) {
1119
- const strategy = strategyOrder[index];
1120
- if (!strategy)
1121
- continue;
1122
- const route = index === 0 ? "primary" : "fallback";
1123
- if (!supportsStrategy(providerKey, strategy)) {
1124
- attemptLogs.push(`${strategy}: unsupported by ${providerKey}`);
1125
- continue;
1126
- }
1127
- if (!hasCredentialsForStrategy(strategy, config, secrets)) {
1128
- attemptLogs.push(`${strategy}: missing credentials`);
1129
- continue;
1130
- }
1131
- const provider = createHealthProviderForStrategy(providerKey, strategy, route, config, secrets);
1132
- if (provider) {
1133
- return provider;
1134
- }
1135
- attemptLogs.push(`${strategy}: not available`);
1136
- }
1137
- throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${attemptLogs.join(", ")}.`);
1138
- }
1139
- function createHealthProviderForStrategy(providerKey, strategy, route, config, secrets) {
1140
- const options = {
1141
- transport: strategy,
1142
- apiBaseUrl: config.apiBaseUrl,
1143
- mcpUrl: config.mcpUrl,
1144
- apiKey: getSecretString(secrets, "apiKey"),
1145
- accessToken: getSecretString(secrets, "accessToken"),
1146
- mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
1147
- webhookSecret: getSecretString(secrets, "webhookSecret"),
1148
- route,
1149
- oauth: {
1150
- tokenUrl: config.oauthTokenUrl,
1151
- refreshToken: getSecretString(secrets, "refreshToken"),
1152
- clientId: getSecretString(secrets, "clientId"),
1153
- clientSecret: getSecretString(secrets, "clientSecret"),
1154
- tokenExpiresAt: getSecretString(secrets, "tokenExpiresAt")
1155
- }
1156
- };
1157
- if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
1158
- return createAggregatorProvider(providerKey, {
1159
- ...options,
1160
- aggregatorKey: "health.openwearables"
1161
- });
1162
- }
1163
- if (strategy === "unofficial") {
1164
- if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
1165
- return;
1166
- }
1167
- if (providerKey !== "health.myfitnesspal" && providerKey !== "health.eightsleep" && providerKey !== "health.peloton" && providerKey !== "health.garmin") {
1168
- return;
1169
- }
1170
- return new UnofficialHealthAutomationProvider({
1171
- ...options,
1172
- providerKey
1173
- });
1174
- }
1175
- if (strategy === "official-mcp") {
1176
- return createOfficialProvider(providerKey, {
1177
- ...options,
1178
- transport: "official-mcp"
1179
- });
1180
- }
1181
- return createOfficialProvider(providerKey, options);
1182
- }
1183
- function createAggregatorProvider(providerKey, options) {
1184
- if (providerKey === "health.apple-health") {
1185
- return new AppleHealthBridgeProvider(options);
1186
- }
1187
- if (providerKey === "health.garmin") {
1188
- return new GarminHealthProvider(options);
1189
- }
1190
- if (providerKey === "health.myfitnesspal") {
1191
- return new MyFitnessPalHealthProvider(options);
1192
- }
1193
- if (providerKey === "health.eightsleep") {
1194
- return new EightSleepHealthProvider(options);
1195
- }
1196
- if (providerKey === "health.peloton") {
1197
- return new PelotonHealthProvider(options);
1198
- }
1199
- if (providerKey === "health.openwearables") {
1200
- return new OpenWearablesHealthProvider(options);
1201
- }
1202
- return new OpenWearablesHealthProvider({
1203
- ...options,
1204
- providerKey,
1205
- upstreamProvider: providerKey.replace("health.", "")
1206
- });
1207
- }
1208
- function createOfficialProvider(providerKey, options) {
1209
- switch (providerKey) {
1210
- case "health.openwearables":
1211
- return new OpenWearablesHealthProvider(options);
1212
- case "health.whoop":
1213
- return new WhoopHealthProvider(options);
1214
- case "health.apple-health":
1215
- return new AppleHealthBridgeProvider(options);
1216
- case "health.oura":
1217
- return new OuraHealthProvider(options);
1218
- case "health.strava":
1219
- return new StravaHealthProvider(options);
1220
- case "health.garmin":
1221
- return new GarminHealthProvider(options);
1222
- case "health.fitbit":
1223
- return new FitbitHealthProvider(options);
1224
- case "health.myfitnesspal":
1225
- return new MyFitnessPalHealthProvider({
1226
- ...options,
1227
- transport: "aggregator-api"
1228
- });
1229
- case "health.eightsleep":
1230
- return new EightSleepHealthProvider({
1231
- ...options,
1232
- transport: "aggregator-api"
1233
- });
1234
- case "health.peloton":
1235
- return new PelotonHealthProvider({
1236
- ...options,
1237
- transport: "aggregator-api"
1238
- });
1239
- default:
1240
- throw new Error(`Unsupported health provider key: ${providerKey}`);
1241
- }
1242
- }
1243
- function toFactoryConfig(config) {
1244
- if (!config || typeof config !== "object" || Array.isArray(config)) {
1245
- return {};
1246
- }
1247
- const record = config;
1248
- return {
1249
- apiBaseUrl: asString(record.apiBaseUrl),
1250
- mcpUrl: asString(record.mcpUrl),
1251
- oauthTokenUrl: asString(record.oauthTokenUrl),
1252
- defaultTransport: normalizeTransport(record.defaultTransport),
1253
- strategyOrder: normalizeTransportArray(record.strategyOrder),
1254
- allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
1255
- unofficialAllowList: Array.isArray(record.unofficialAllowList) ? record.unofficialAllowList.map((item) => typeof item === "string" ? item : undefined).filter((item) => Boolean(item)) : undefined
1256
- };
1257
- }
1258
- function buildStrategyOrder(config) {
1259
- const order = resolveHealthStrategyOrder(config);
1260
- if (!config.defaultTransport) {
1261
- return order;
1262
- }
1263
- const withoutDefault = order.filter((item) => item !== config.defaultTransport);
1264
- return [config.defaultTransport, ...withoutDefault];
1265
- }
1266
- function normalizeTransport(value) {
1267
- if (typeof value !== "string")
1268
- return;
1269
- if (value === "official-api" || value === "official-mcp" || value === "aggregator-api" || value === "aggregator-mcp" || value === "unofficial") {
1270
- return value;
1271
- }
1272
- return;
1273
- }
1274
- function normalizeTransportArray(value) {
1275
- if (!Array.isArray(value))
1276
- return;
1277
- const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
1278
- return transports.length > 0 ? transports : undefined;
1279
- }
1280
- function supportsStrategy(providerKey, strategy) {
1281
- if (strategy === "official-api" || strategy === "official-mcp") {
1282
- return OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER[providerKey];
1283
- }
1284
- if (strategy === "unofficial") {
1285
- return UNOFFICIAL_SUPPORTED_BY_PROVIDER[providerKey];
1286
- }
1287
- return true;
1288
- }
1289
- function hasCredentialsForStrategy(strategy, config, secrets) {
1290
- const hasApiCredential = Boolean(getSecretString(secrets, "accessToken")) || Boolean(getSecretString(secrets, "apiKey"));
1291
- const hasMcpCredential = Boolean(getSecretString(secrets, "mcpAccessToken")) || hasApiCredential;
1292
- if (strategy === "official-api" || strategy === "aggregator-api") {
1293
- return hasApiCredential;
1294
- }
1295
- if (strategy === "official-mcp" || strategy === "aggregator-mcp") {
1296
- return Boolean(config.mcpUrl) && hasMcpCredential;
1297
- }
1298
- const hasAutomationCredential = hasMcpCredential || Boolean(getSecretString(secrets, "username")) && Boolean(getSecretString(secrets, "password"));
1299
- return Boolean(config.mcpUrl) && hasAutomationCredential;
1300
- }
1301
- function getSecretString(secrets, key) {
1302
- const value = secrets[key];
1303
- return typeof value === "string" && value.trim().length > 0 ? value : undefined;
1304
- }
1305
- function asString(value) {
1306
- return typeof value === "string" && value.trim().length > 0 ? value : undefined;
1307
- }
1308
- export {
1309
- createHealthProviderFromContext
1310
- };
2
+ var ut=import.meta.require;var j=["items","data","records","activities","workouts","sleep","biometrics","nutrition"];function l(t){if(!t||typeof t!=="object"||Array.isArray(t))return;return t}function W(t){return Array.isArray(t)?t:void 0}function o(t,e){if(!t)return;for(let r of e){let i=t[r];if(typeof i==="string"&&i.trim().length>0)return i}return}function a(t,e){if(!t)return;for(let r of e){let i=t[r];if(typeof i==="number"&&Number.isFinite(i))return i;if(typeof i==="string"&&i.trim().length>0){let n=Number(i);if(Number.isFinite(n))return n}}return}function N(t,e){if(!t)return;for(let r of e){let i=t[r];if(typeof i==="boolean")return i}return}function K(t,e=j){let r=l(t);if(!r)return W(t)?.map((i)=>l(i)).filter((i)=>Boolean(i))??[];for(let i of e){let n=W(r[i]);if(!n)continue;return n.map((s)=>l(s)).filter((s)=>Boolean(s))}return[]}function $(t){let e=l(t),r=l(e?.pagination),i=o(r,["nextCursor","next_cursor"])??o(e,["nextCursor","next_cursor","cursor","next_page_token"]),n=N(r,["hasMore","has_more"])??N(e,["hasMore","has_more"]);return{nextCursor:i,hasMore:n??Boolean(i)}}function m(t,e,r="activity"){let i=o(t,["external_id","externalId","uuid","id"])??`${e.providerKey}:${r}`;return{id:o(t,["id","uuid","workout_id"])??`${e.providerKey}:activity:${i}`,externalId:i,tenantId:e.tenantId,connectionId:e.connectionId??"unknown",userId:o(t,["user_id","userId","athlete_id"]),providerKey:e.providerKey,activityType:o(t,["activity_type","type","sport_type","sport"])??r,startedAt:f(t,["started_at","start_time","start_date","created_at"]),endedAt:f(t,["ended_at","end_time"]),durationSeconds:a(t,["duration_seconds","duration","elapsed_time"]),distanceMeters:a(t,["distance_meters","distance"]),caloriesKcal:a(t,["calories_kcal","calories","active_kilocalories"]),steps:a(t,["steps"])?.valueOf(),metadata:t}}function P(t,e,r="workout"){let i=m(t,e,r);return{id:i.id,externalId:i.externalId,tenantId:i.tenantId,connectionId:i.connectionId,userId:i.userId,providerKey:i.providerKey,workoutType:o(t,["workout_type","sport_type","type","activity_type"])??r,startedAt:i.startedAt,endedAt:i.endedAt,durationSeconds:i.durationSeconds,distanceMeters:i.distanceMeters,caloriesKcal:i.caloriesKcal,averageHeartRateBpm:a(t,["average_heart_rate","avg_hr","average_heart_rate_bpm"]),maxHeartRateBpm:a(t,["max_heart_rate","max_hr","max_heart_rate_bpm"]),metadata:t}}function v(t,e){let r=o(t,["external_id","externalId","uuid","id"])??`${e.providerKey}:sleep`,i=o(t,["id","uuid"])??`${e.providerKey}:sleep:${r}`,n=f(t,["started_at","start_time","bedtime_start","start"])??new Date(0).toISOString(),s=f(t,["ended_at","end_time","bedtime_end","end"])??n;return{id:i,externalId:r,tenantId:e.tenantId,connectionId:e.connectionId??"unknown",userId:o(t,["user_id","userId"]),providerKey:e.providerKey,startedAt:n,endedAt:s,durationSeconds:a(t,["duration_seconds","duration","total_sleep_duration"]),deepSleepSeconds:a(t,["deep_sleep_seconds","deep_sleep_duration"]),lightSleepSeconds:a(t,["light_sleep_seconds","light_sleep_duration"]),remSleepSeconds:a(t,["rem_sleep_seconds","rem_sleep_duration"]),awakeSeconds:a(t,["awake_seconds","awake_time"]),sleepScore:a(t,["sleep_score","score"]),metadata:t}}function H(t,e,r="metric"){let i=o(t,["external_id","externalId","uuid","id"])??`${e.providerKey}:biometric`;return{id:o(t,["id","uuid"])??`${e.providerKey}:biometric:${i}`,externalId:i,tenantId:e.tenantId,connectionId:e.connectionId??"unknown",userId:o(t,["user_id","userId"]),providerKey:e.providerKey,metricType:o(t,["metric_type","metric","type","name"])??r,value:a(t,["value","score","measurement"])??0,unit:o(t,["unit"]),measuredAt:f(t,["measured_at","timestamp","created_at"])??new Date().toISOString(),metadata:t}}function k(t,e){let r=o(t,["external_id","externalId","uuid","id"])??`${e.providerKey}:nutrition`;return{id:o(t,["id","uuid"])??`${e.providerKey}:nutrition:${r}`,externalId:r,tenantId:e.tenantId,connectionId:e.connectionId??"unknown",userId:o(t,["user_id","userId"]),providerKey:e.providerKey,loggedAt:f(t,["logged_at","created_at","date","timestamp"])??new Date().toISOString(),caloriesKcal:a(t,["calories_kcal","calories"]),proteinGrams:a(t,["protein_grams","protein"]),carbsGrams:a(t,["carbs_grams","carbs"]),fatGrams:a(t,["fat_grams","fat"]),fiberGrams:a(t,["fiber_grams","fiber"]),hydrationMl:a(t,["hydration_ml","water_ml","water"]),metadata:t}}function E(t,e,r){let i=l(t),n=o(i,["status","connection_status","health"])??"healthy";return{tenantId:e.tenantId,connectionId:e.connectionId,status:n==="healthy"||n==="degraded"||n==="error"||n==="disconnected"?n:"healthy",source:r,lastCheckedAt:f(i,["last_checked_at","lastCheckedAt"])??new Date().toISOString(),errorCode:o(i,["error_code","errorCode"]),errorMessage:o(i,["error_message","errorMessage"]),metadata:l(i?.metadata)}}function M(t,e,r){let i=l(t),n=o(i,["entity_type","entityType","type"]),s=n==="activity"||n==="workout"||n==="sleep"||n==="biometric"||n==="nutrition"?n:void 0;return{providerKey:e,eventType:o(i,["event_type","eventType","event"]),externalEntityId:o(i,["external_entity_id","externalEntityId","entity_id","entityId","id"]),entityType:s,receivedAt:new Date().toISOString(),verified:r,payload:t,metadata:l(i?.metadata)}}function f(t,e){let r=o(t,e);if(!r)return;let i=new Date(r);if(Number.isNaN(i.getTime()))return;return i.toISOString()}class Q extends Error{code="NOT_SUPPORTED";constructor(t){super(t);this.name="HealthProviderCapabilityError"}}class y{providerKey;transport;apiBaseUrl;mcpUrl;apiKey;accessToken;refreshToken;mcpAccessToken;webhookSecret;webhookSignatureHeader;route;aggregatorKey;oauth;fetchFn;mcpRequestId=0;constructor(t){this.providerKey=t.providerKey,this.transport=t.transport,this.apiBaseUrl=t.apiBaseUrl,this.mcpUrl=t.mcpUrl,this.apiKey=t.apiKey,this.accessToken=t.accessToken,this.refreshToken=t.oauth?.refreshToken,this.mcpAccessToken=t.mcpAccessToken,this.webhookSecret=t.webhookSecret,this.webhookSignatureHeader=t.webhookSignatureHeader??"x-webhook-signature",this.route=t.route??"primary",this.aggregatorKey=t.aggregatorKey,this.oauth=t.oauth??{},this.fetchFn=t.fetchFn??fetch}async listActivities(t){throw this.unsupported("activities")}async listWorkouts(t){throw this.unsupported("workouts")}async listSleep(t){throw this.unsupported("sleep")}async listBiometrics(t){throw this.unsupported("biometrics")}async listNutrition(t){throw this.unsupported("nutrition")}async getConnectionStatus(t){return this.fetchConnectionStatus(t,{mcpTool:`${this.providerSlug()}_connection_status`})}async syncActivities(t){return this.syncFromList(()=>this.listActivities(t))}async syncWorkouts(t){return this.syncFromList(()=>this.listWorkouts(t))}async syncSleep(t){return this.syncFromList(()=>this.listSleep(t))}async syncBiometrics(t){return this.syncFromList(()=>this.listBiometrics(t))}async syncNutrition(t){return this.syncFromList(()=>this.listNutrition(t))}async parseWebhook(t){let e=t.parsedBody??z(t.rawBody),r=await this.verifyWebhook(t);return M(e,this.providerKey,r)}async verifyWebhook(t){if(!this.webhookSecret)return!0;return q(t.headers,this.webhookSignatureHeader)===this.webhookSecret}async fetchActivities(t,e){let r=await this.fetchList(t,e);return{activities:r.items,nextCursor:r.nextCursor,hasMore:r.hasMore,source:this.currentSource()}}async fetchWorkouts(t,e){let r=await this.fetchList(t,e);return{workouts:r.items,nextCursor:r.nextCursor,hasMore:r.hasMore,source:this.currentSource()}}async fetchSleep(t,e){let r=await this.fetchList(t,e);return{sleep:r.items,nextCursor:r.nextCursor,hasMore:r.hasMore,source:this.currentSource()}}async fetchBiometrics(t,e){let r=await this.fetchList(t,e);return{biometrics:r.items,nextCursor:r.nextCursor,hasMore:r.hasMore,source:this.currentSource()}}async fetchNutrition(t,e){let r=await this.fetchList(t,e);return{nutrition:r.items,nextCursor:r.nextCursor,hasMore:r.hasMore,source:this.currentSource()}}async fetchConnectionStatus(t,e){let r=await this.fetchPayload(e,t);return E(r,t,this.currentSource())}currentSource(){return{providerKey:this.providerKey,transport:this.transport,route:this.route,aggregatorKey:this.aggregatorKey}}providerSlug(){return this.providerKey.replace("health.","").replace(/-/g,"_")}unsupported(t){return new Q(`${this.providerKey} does not support ${t}`)}async syncFromList(t){let e=await t();return{synced:G(e),failed:0,nextCursor:void 0,source:e.source}}async fetchList(t,e){let r=await this.fetchPayload(e,t),i=K(r,e.listKeys).map((s)=>e.mapItem(s,t)).filter((s)=>Boolean(s)),n=$(r);return{items:i,nextCursor:n.nextCursor,hasMore:n.hasMore}}async fetchPayload(t,e){let r=t.method??"GET",i=t.buildQuery?.(e),n=t.buildBody?.(e);if(this.isMcpTransport())return this.callMcpTool(t.mcpTool,{...i??{},...n??{}});if(!t.apiPath||!this.apiBaseUrl)throw Error(`${this.providerKey} transport is missing an API path.`);if(r==="POST")return this.requestApi(t.apiPath,"POST",void 0,n);return this.requestApi(t.apiPath,"GET",i,void 0)}isMcpTransport(){return this.transport.endsWith("mcp")||this.transport==="unofficial"}async requestApi(t,e,r,i){let n=new URL(t,V(this.apiBaseUrl??""));if(r)for(let[p,h]of Object.entries(r)){if(h==null)continue;if(Array.isArray(h)){h.forEach((x)=>{if(x!=null)n.searchParams.append(p,String(x))});continue}n.searchParams.set(p,String(h))}let s=await this.fetchFn(n,{method:e,headers:this.authorizationHeaders(),body:e==="POST"?JSON.stringify(i??{}):void 0});if(s.status===401&&await this.refreshAccessToken()){let p=await this.fetchFn(n,{method:e,headers:this.authorizationHeaders(),body:e==="POST"?JSON.stringify(i??{}):void 0});return this.readResponsePayload(p,t)}return this.readResponsePayload(s,t)}async callMcpTool(t,e){if(!this.mcpUrl)throw Error(`${this.providerKey} MCP URL is not configured.`);let r=await this.fetchFn(this.mcpUrl,{method:"POST",headers:{"Content-Type":"application/json",...this.mcpAccessToken?{Authorization:`Bearer ${this.mcpAccessToken}`}:{}},body:JSON.stringify({jsonrpc:"2.0",id:++this.mcpRequestId,method:"tools/call",params:{name:t,arguments:e}})}),i=await this.readResponsePayload(r,t),n=l(i);if(!n)return i;let s=l(n.result);if(s)return s.structuredContent??s.data??s;return n.structuredContent??n.data??n}authorizationHeaders(){let t=this.accessToken??this.apiKey;return{"Content-Type":"application/json",...t?{Authorization:`Bearer ${t}`}:{}}}async refreshAccessToken(){if(!this.oauth.tokenUrl||!this.refreshToken)return!1;let t=new URL(this.oauth.tokenUrl),e=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.refreshToken,...this.oauth.clientId?{client_id:this.oauth.clientId}:{},...this.oauth.clientSecret?{client_secret:this.oauth.clientSecret}:{}}),r=await this.fetchFn(t,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:e.toString()});if(!r.ok)return!1;let i=await r.json();if(this.accessToken=i.access_token,this.refreshToken=i.refresh_token??this.refreshToken,typeof i.expires_in==="number")this.oauth.tokenExpiresAt=new Date(Date.now()+i.expires_in*1000).toISOString();return Boolean(this.accessToken)}async readResponsePayload(t,e){if(!t.ok){let r=await J(t);throw Error(`${this.providerKey} request ${e} failed (${t.status}): ${r}`)}if(t.status===204)return{};return t.json()}}function q(t,e){let r=e.toLowerCase(),i=Object.entries(t).find(([s])=>s.toLowerCase()===r);if(!i)return;let n=i[1];return Array.isArray(n)?n[0]:n}function G(t){let e=["activities","workouts","sleep","biometrics","nutrition"];for(let r of e){let i=t[r];if(Array.isArray(i))return i.length}return 0}function V(t){return t.endsWith("/")?t:`${t}/`}function z(t){try{return JSON.parse(t)}catch{return{rawBody:t}}}async function J(t){try{return await t.text()}catch{return t.statusText}}function c(t){return{tenantId:t.tenantId,connectionId:t.connectionId,userId:t.userId,from:t.from,to:t.to,cursor:t.cursor,pageSize:t.pageSize}}function T(t){return{...c(t),metricTypes:t.metricTypes}}class d extends y{upstreamProvider;constructor(t){super({providerKey:t.providerKey??"health.openwearables",apiBaseUrl:t.apiBaseUrl??"https://api.openwearables.io",webhookSignatureHeader:"x-openwearables-signature",...t});this.upstreamProvider=t.upstreamProvider}async listActivities(t){return this.fetchActivities(t,{apiPath:"/v1/activities",mcpTool:"openwearables_list_activities",buildQuery:(e)=>this.withUpstreamProvider(c(e)),mapItem:(e,r)=>m(e,this.context(r),"activity")})}async listWorkouts(t){return this.fetchWorkouts(t,{apiPath:"/v1/workouts",mcpTool:"openwearables_list_workouts",buildQuery:(e)=>this.withUpstreamProvider(c(e)),mapItem:(e,r)=>P(e,this.context(r))})}async listSleep(t){return this.fetchSleep(t,{apiPath:"/v1/sleep",mcpTool:"openwearables_list_sleep",buildQuery:(e)=>this.withUpstreamProvider(c(e)),mapItem:(e,r)=>v(e,this.context(r))})}async listBiometrics(t){return this.fetchBiometrics(t,{apiPath:"/v1/biometrics",mcpTool:"openwearables_list_biometrics",buildQuery:(e)=>this.withUpstreamProvider(T(e)),mapItem:(e,r)=>H(e,this.context(r))})}async listNutrition(t){return this.fetchNutrition(t,{apiPath:"/v1/nutrition",mcpTool:"openwearables_list_nutrition",buildQuery:(e)=>this.withUpstreamProvider(c(e)),mapItem:(e,r)=>k(e,this.context(r))})}async getConnectionStatus(t){return this.fetchConnectionStatus(t,{apiPath:`/v1/connections/${encodeURIComponent(t.connectionId)}/status`,mcpTool:"openwearables_connection_status"})}withUpstreamProvider(t){return{...t,...this.upstreamProvider?{upstreamProvider:this.upstreamProvider}:{}}}context(t){return{tenantId:t.tenantId,connectionId:t.connectionId,providerKey:this.providerKey}}}class _ extends d{constructor(t){super({...t,providerKey:"health.apple-health",upstreamProvider:"apple-health"})}}class L extends y{constructor(t){super({providerKey:"health.whoop",apiBaseUrl:t.apiBaseUrl??"https://api.prod.whoop.com",webhookSignatureHeader:"x-whoop-signature",oauth:{tokenUrl:t.oauth?.tokenUrl??"https://api.prod.whoop.com/oauth/oauth2/token",...t.oauth},...t})}async listActivities(t){return this.fetchActivities(t,{apiPath:"/v2/activity/workout",mcpTool:"whoop_list_activities",buildQuery:c,mapItem:(e,r)=>m(e,this.context(r),"workout")})}async listWorkouts(t){return this.fetchWorkouts(t,{apiPath:"/v2/activity/workout",mcpTool:"whoop_list_workouts",buildQuery:c,mapItem:(e,r)=>P(e,this.context(r))})}async listSleep(t){return this.fetchSleep(t,{apiPath:"/v2/activity/sleep",mcpTool:"whoop_list_sleep",buildQuery:c,mapItem:(e,r)=>v(e,this.context(r))})}async listBiometrics(t){return this.fetchBiometrics(t,{apiPath:"/v2/recovery",mcpTool:"whoop_list_biometrics",buildQuery:T,mapItem:(e,r)=>H(e,this.context(r),"recovery_score")})}async listNutrition(t){throw this.unsupported("nutrition")}async getConnectionStatus(t){return this.fetchConnectionStatus(t,{apiPath:"/v2/user/profile/basic",mcpTool:"whoop_connection_status"})}context(t){return{tenantId:t.tenantId,connectionId:t.connectionId,providerKey:this.providerKey}}}class R extends y{constructor(t){super({providerKey:"health.oura",apiBaseUrl:t.apiBaseUrl??"https://api.ouraring.com",webhookSignatureHeader:"x-oura-signature",oauth:{tokenUrl:t.oauth?.tokenUrl??"https://api.ouraring.com/oauth/token",...t.oauth},...t})}async listActivities(t){return this.fetchActivities(t,{apiPath:"/v2/usercollection/daily_activity",mcpTool:"oura_list_activities",buildQuery:c,mapItem:(e,r)=>m(e,this.context(r))})}async listWorkouts(t){return this.fetchWorkouts(t,{apiPath:"/v2/usercollection/workout",mcpTool:"oura_list_workouts",buildQuery:c,mapItem:(e,r)=>P(e,this.context(r))})}async listSleep(t){return this.fetchSleep(t,{apiPath:"/v2/usercollection/sleep",mcpTool:"oura_list_sleep",buildQuery:c,mapItem:(e,r)=>v(e,this.context(r))})}async listBiometrics(t){return this.fetchBiometrics(t,{apiPath:"/v2/usercollection/daily_readiness",mcpTool:"oura_list_biometrics",buildQuery:T,mapItem:(e,r)=>H(e,this.context(r),"readiness_score")})}async listNutrition(t){throw this.unsupported("nutrition")}async getConnectionStatus(t){return this.fetchConnectionStatus(t,{apiPath:"/v2/usercollection/personal_info",mcpTool:"oura_connection_status"})}context(t){return{tenantId:t.tenantId,connectionId:t.connectionId,providerKey:this.providerKey}}}class A extends y{constructor(t){super({providerKey:"health.strava",apiBaseUrl:t.apiBaseUrl??"https://www.strava.com",webhookSignatureHeader:"x-strava-signature",oauth:{tokenUrl:t.oauth?.tokenUrl??"https://www.strava.com/oauth/token",...t.oauth},...t})}async listActivities(t){return this.fetchActivities(t,{apiPath:"/api/v3/athlete/activities",mcpTool:"strava_list_activities",buildQuery:c,mapItem:(e,r)=>m(e,this.context(r))})}async listWorkouts(t){return this.fetchWorkouts(t,{apiPath:"/api/v3/athlete/activities",mcpTool:"strava_list_workouts",buildQuery:c,mapItem:(e,r)=>P(e,this.context(r))})}async listSleep(t){throw this.unsupported("sleep")}async listBiometrics(t){throw this.unsupported("biometrics")}async listNutrition(t){throw this.unsupported("nutrition")}async getConnectionStatus(t){return this.fetchConnectionStatus(t,{apiPath:"/api/v3/athlete",mcpTool:"strava_connection_status"})}context(t){return{tenantId:t.tenantId,connectionId:t.connectionId,providerKey:this.providerKey}}}class B extends y{constructor(t){super({providerKey:"health.fitbit",apiBaseUrl:t.apiBaseUrl??"https://api.fitbit.com",webhookSignatureHeader:"x-fitbit-signature",oauth:{tokenUrl:t.oauth?.tokenUrl??"https://api.fitbit.com/oauth2/token",...t.oauth},...t})}async listActivities(t){return this.fetchActivities(t,{apiPath:"/1/user/-/activities/list.json",mcpTool:"fitbit_list_activities",buildQuery:c,mapItem:(e,r)=>m(e,this.context(r))})}async listWorkouts(t){return this.fetchWorkouts(t,{apiPath:"/1/user/-/activities/list.json",mcpTool:"fitbit_list_workouts",buildQuery:c,mapItem:(e,r)=>P(e,this.context(r))})}async listSleep(t){return this.fetchSleep(t,{apiPath:"/1.2/user/-/sleep/list.json",mcpTool:"fitbit_list_sleep",buildQuery:c,mapItem:(e,r)=>v(e,this.context(r))})}async listBiometrics(t){return this.fetchBiometrics(t,{apiPath:"/1/user/-/body/log/weight/date/today/1m.json",mcpTool:"fitbit_list_biometrics",buildQuery:T,mapItem:(e,r)=>H(e,this.context(r),"weight")})}async listNutrition(t){return this.fetchNutrition(t,{apiPath:"/1/user/-/foods/log/date/today.json",mcpTool:"fitbit_list_nutrition",buildQuery:c,mapItem:(e,r)=>k(e,this.context(r))})}async getConnectionStatus(t){return this.fetchConnectionStatus(t,{apiPath:"/1/user/-/profile.json",mcpTool:"fitbit_connection_status"})}context(t){return{tenantId:t.tenantId,connectionId:t.connectionId,providerKey:this.providerKey}}}var Y={"health.garmin":"garmin","health.myfitnesspal":"myfitnesspal","health.eightsleep":"eightsleep","health.peloton":"peloton"};function S(t){return{tenantId:t.tenantId,connectionId:t.connectionId,userId:t.userId,from:t.from,to:t.to,cursor:t.cursor,pageSize:t.pageSize}}class w extends d{constructor(t){super({...t,providerKey:"health.garmin",upstreamProvider:"garmin"})}}class g extends d{constructor(t){super({...t,providerKey:"health.myfitnesspal",upstreamProvider:"myfitnesspal"})}}class I extends d{constructor(t){super({...t,providerKey:"health.eightsleep",upstreamProvider:"eightsleep"})}}class b extends d{constructor(t){super({...t,providerKey:"health.peloton",upstreamProvider:"peloton"})}}class U extends y{providerSlugValue;constructor(t){super({...t,providerKey:t.providerKey,webhookSignatureHeader:"x-unofficial-signature"});this.providerSlugValue=Y[t.providerKey]}async listActivities(t){return this.fetchActivities(t,{mcpTool:`${this.providerSlugValue}_list_activities`,buildQuery:S,mapItem:(e,r)=>m(e,this.context(r),"activity")})}async listWorkouts(t){return this.fetchWorkouts(t,{mcpTool:`${this.providerSlugValue}_list_workouts`,buildQuery:S,mapItem:(e,r)=>P(e,this.context(r))})}async listSleep(t){return this.fetchSleep(t,{mcpTool:`${this.providerSlugValue}_list_sleep`,buildQuery:S,mapItem:(e,r)=>v(e,this.context(r))})}async listBiometrics(t){return this.fetchBiometrics(t,{mcpTool:`${this.providerSlugValue}_list_biometrics`,buildQuery:(e)=>({...S(e),metricTypes:e.metricTypes}),mapItem:(e,r)=>H(e,this.context(r))})}async listNutrition(t){return this.fetchNutrition(t,{mcpTool:`${this.providerSlugValue}_list_nutrition`,buildQuery:S,mapItem:(e,r)=>k(e,this.context(r))})}async getConnectionStatus(t){return this.fetchConnectionStatus(t,{mcpTool:`${this.providerSlugValue}_connection_status`})}context(t){return{tenantId:t.tenantId,connectionId:t.connectionId,providerKey:this.providerKey}}}import{isUnofficialHealthProviderAllowed as X,resolveHealthStrategyOrder as Z}from"@contractspec/integration.runtime/runtime";var tt={"health.openwearables":!1,"health.whoop":!0,"health.apple-health":!1,"health.oura":!0,"health.strava":!0,"health.garmin":!1,"health.fitbit":!0,"health.myfitnesspal":!1,"health.eightsleep":!1,"health.peloton":!1},et={"health.openwearables":!1,"health.whoop":!1,"health.apple-health":!1,"health.oura":!1,"health.strava":!1,"health.garmin":!0,"health.fitbit":!1,"health.myfitnesspal":!0,"health.eightsleep":!0,"health.peloton":!0};function bt(t,e){let r=t.spec.meta.key,i=nt(t.config),n=st(i),s=[];for(let p=0;p<n.length;p+=1){let h=n[p];if(!h)continue;let x=p===0?"primary":"fallback";if(!at(r,h)){s.push(`${h}: unsupported by ${r}`);continue}if(!ct(h,i,e)){s.push(`${h}: missing credentials`);continue}let O=rt(r,h,x,i,e);if(O)return O;s.push(`${h}: not available`)}throw Error(`Unable to resolve health provider for ${r}. Strategies attempted: ${s.join(", ")}.`)}function rt(t,e,r,i,n){let s={transport:e,apiBaseUrl:i.apiBaseUrl,mcpUrl:i.mcpUrl,apiKey:u(n,"apiKey"),accessToken:u(n,"accessToken"),mcpAccessToken:u(n,"mcpAccessToken"),webhookSecret:u(n,"webhookSecret"),route:r,oauth:{tokenUrl:i.oauthTokenUrl,refreshToken:u(n,"refreshToken"),clientId:u(n,"clientId"),clientSecret:u(n,"clientSecret"),tokenExpiresAt:u(n,"tokenExpiresAt")}};if(e==="aggregator-api"||e==="aggregator-mcp")return it(t,{...s,aggregatorKey:"health.openwearables"});if(e==="unofficial"){if(!X(t,i))return;if(t!=="health.myfitnesspal"&&t!=="health.eightsleep"&&t!=="health.peloton"&&t!=="health.garmin")return;return new U({...s,providerKey:t})}if(e==="official-mcp")return F(t,{...s,transport:"official-mcp"});return F(t,s)}function it(t,e){if(t==="health.apple-health")return new _(e);if(t==="health.garmin")return new w(e);if(t==="health.myfitnesspal")return new g(e);if(t==="health.eightsleep")return new I(e);if(t==="health.peloton")return new b(e);if(t==="health.openwearables")return new d(e);return new d({...e,providerKey:t,upstreamProvider:t.replace("health.","")})}function F(t,e){switch(t){case"health.openwearables":return new d(e);case"health.whoop":return new L(e);case"health.apple-health":return new _(e);case"health.oura":return new R(e);case"health.strava":return new A(e);case"health.garmin":return new w(e);case"health.fitbit":return new B(e);case"health.myfitnesspal":return new g({...e,transport:"aggregator-api"});case"health.eightsleep":return new I({...e,transport:"aggregator-api"});case"health.peloton":return new b({...e,transport:"aggregator-api"});default:throw Error(`Unsupported health provider key: ${t}`)}}function nt(t){if(!t||typeof t!=="object"||Array.isArray(t))return{};let e=t;return{apiBaseUrl:C(e.apiBaseUrl),mcpUrl:C(e.mcpUrl),oauthTokenUrl:C(e.oauthTokenUrl),defaultTransport:D(e.defaultTransport),strategyOrder:ot(e.strategyOrder),allowUnofficial:typeof e.allowUnofficial==="boolean"?e.allowUnofficial:!1,unofficialAllowList:Array.isArray(e.unofficialAllowList)?e.unofficialAllowList.map((r)=>typeof r==="string"?r:void 0).filter((r)=>Boolean(r)):void 0}}function st(t){let e=Z(t);if(!t.defaultTransport)return e;let r=e.filter((i)=>i!==t.defaultTransport);return[t.defaultTransport,...r]}function D(t){if(typeof t!=="string")return;if(t==="official-api"||t==="official-mcp"||t==="aggregator-api"||t==="aggregator-mcp"||t==="unofficial")return t;return}function ot(t){if(!Array.isArray(t))return;let e=t.map((r)=>D(r)).filter((r)=>Boolean(r));return e.length>0?e:void 0}function at(t,e){if(e==="official-api"||e==="official-mcp")return tt[t];if(e==="unofficial")return et[t];return!0}function ct(t,e,r){let i=Boolean(u(r,"accessToken"))||Boolean(u(r,"apiKey")),n=Boolean(u(r,"mcpAccessToken"))||i;if(t==="official-api"||t==="aggregator-api")return i;if(t==="official-mcp"||t==="aggregator-mcp")return Boolean(e.mcpUrl)&&n;let s=n||Boolean(u(r,"username"))&&Boolean(u(r,"password"));return Boolean(e.mcpUrl)&&s}function u(t,e){let r=t[e];return typeof r==="string"&&r.trim().length>0?r:void 0}function C(t){return typeof t==="string"&&t.trim().length>0?t:void 0}export{bt as createHealthProviderFromContext};