@contractspec/integration.providers-impls 2.10.0 → 3.1.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 (138) hide show
  1. package/README.md +66 -10
  2. package/dist/impls/async-event-queue.d.ts +8 -0
  3. package/dist/impls/async-event-queue.js +49 -0
  4. package/dist/impls/composio-fallback-resolver.d.ts +34 -0
  5. package/dist/impls/composio-fallback-resolver.js +580 -0
  6. package/dist/impls/composio-mcp.d.ts +22 -0
  7. package/dist/impls/composio-mcp.js +164 -0
  8. package/dist/impls/composio-proxies.d.ts +60 -0
  9. package/dist/impls/composio-proxies.js +311 -0
  10. package/dist/impls/composio-sdk.d.ts +25 -0
  11. package/dist/impls/composio-sdk.js +78 -0
  12. package/dist/impls/composio-types.d.ts +43 -0
  13. package/dist/impls/composio-types.js +54 -0
  14. package/dist/impls/elevenlabs-voice.js +2 -0
  15. package/dist/impls/fal-voice.js +2 -0
  16. package/dist/impls/fathom-meeting-recorder.js +2 -0
  17. package/dist/impls/fathom-meeting-recorder.mapper.js +2 -0
  18. package/dist/impls/fathom-meeting-recorder.utils.js +2 -0
  19. package/dist/impls/fathom-meeting-recorder.webhooks.js +2 -0
  20. package/dist/impls/fireflies-meeting-recorder.js +2 -0
  21. package/dist/impls/fireflies-meeting-recorder.queries.js +2 -0
  22. package/dist/impls/fireflies-meeting-recorder.utils.js +2 -0
  23. package/dist/impls/gcs-storage.js +2 -0
  24. package/dist/impls/gmail-inbound.js +2 -0
  25. package/dist/impls/gmail-outbound.js +2 -0
  26. package/dist/impls/google-calendar.js +2 -0
  27. package/dist/impls/gradium-voice.js +2 -0
  28. package/dist/impls/granola-meeting-recorder.js +2 -0
  29. package/dist/impls/granola-meeting-recorder.mcp.js +2 -0
  30. package/dist/impls/health/base-health-provider.d.ts +64 -13
  31. package/dist/impls/health/base-health-provider.js +508 -156
  32. package/dist/impls/health/hybrid-health-providers.d.ts +34 -0
  33. package/dist/impls/health/hybrid-health-providers.js +1090 -0
  34. package/dist/impls/health/official-health-providers.d.ts +78 -0
  35. package/dist/impls/health/official-health-providers.js +970 -0
  36. package/dist/impls/health/provider-normalizers.d.ts +28 -0
  37. package/dist/impls/health/provider-normalizers.js +289 -0
  38. package/dist/impls/health/providers.d.ts +2 -39
  39. package/dist/impls/health/providers.js +897 -184
  40. package/dist/impls/health-provider-factory.js +1011 -196
  41. package/dist/impls/index.d.ts +11 -0
  42. package/dist/impls/index.js +2588 -259
  43. package/dist/impls/jira.js +2 -0
  44. package/dist/impls/linear.js +2 -0
  45. package/dist/impls/messaging-github.d.ts +17 -0
  46. package/dist/impls/messaging-github.js +112 -0
  47. package/dist/impls/messaging-slack.d.ts +14 -0
  48. package/dist/impls/messaging-slack.js +82 -0
  49. package/dist/impls/messaging-whatsapp-meta.d.ts +13 -0
  50. package/dist/impls/messaging-whatsapp-meta.js +54 -0
  51. package/dist/impls/messaging-whatsapp-twilio.d.ts +13 -0
  52. package/dist/impls/messaging-whatsapp-twilio.js +84 -0
  53. package/dist/impls/mistral-conversational.d.ts +23 -0
  54. package/dist/impls/mistral-conversational.js +478 -0
  55. package/dist/impls/mistral-conversational.session.d.ts +32 -0
  56. package/dist/impls/mistral-conversational.session.js +208 -0
  57. package/dist/impls/mistral-embedding.js +2 -0
  58. package/dist/impls/mistral-llm.js +2 -0
  59. package/dist/impls/mistral-stt.d.ts +17 -0
  60. package/dist/impls/mistral-stt.js +169 -0
  61. package/dist/impls/notion.js +2 -0
  62. package/dist/impls/posthog-reader.js +2 -0
  63. package/dist/impls/posthog-utils.js +2 -0
  64. package/dist/impls/posthog.js +2 -0
  65. package/dist/impls/postmark-email.js +2 -0
  66. package/dist/impls/powens-client.js +2 -0
  67. package/dist/impls/powens-openbanking.js +2 -0
  68. package/dist/impls/provider-factory.d.ts +29 -1
  69. package/dist/impls/provider-factory.js +1985 -249
  70. package/dist/impls/qdrant-vector.js +2 -0
  71. package/dist/impls/stripe-payments.js +3 -1
  72. package/dist/impls/supabase-psql.js +2 -0
  73. package/dist/impls/supabase-vector.js +2 -0
  74. package/dist/impls/tldv-meeting-recorder.js +2 -0
  75. package/dist/impls/twilio-sms.js +2 -0
  76. package/dist/index.d.ts +1 -0
  77. package/dist/index.js +2615 -283
  78. package/dist/messaging.d.ts +1 -0
  79. package/dist/messaging.js +3 -0
  80. package/dist/node/impls/async-event-queue.js +49 -0
  81. package/dist/node/impls/composio-fallback-resolver.js +580 -0
  82. package/dist/node/impls/composio-mcp.js +164 -0
  83. package/dist/node/impls/composio-proxies.js +311 -0
  84. package/dist/node/impls/composio-sdk.js +78 -0
  85. package/dist/node/impls/composio-types.js +54 -0
  86. package/dist/node/impls/elevenlabs-voice.js +3 -0
  87. package/dist/node/impls/fal-voice.js +3 -0
  88. package/dist/node/impls/fathom-meeting-recorder.js +3 -0
  89. package/dist/node/impls/fathom-meeting-recorder.mapper.js +3 -0
  90. package/dist/node/impls/fathom-meeting-recorder.utils.js +3 -0
  91. package/dist/node/impls/fathom-meeting-recorder.webhooks.js +3 -0
  92. package/dist/node/impls/fireflies-meeting-recorder.js +3 -0
  93. package/dist/node/impls/fireflies-meeting-recorder.queries.js +3 -0
  94. package/dist/node/impls/fireflies-meeting-recorder.utils.js +3 -0
  95. package/dist/node/impls/gcs-storage.js +3 -0
  96. package/dist/node/impls/gmail-inbound.js +3 -0
  97. package/dist/node/impls/gmail-outbound.js +3 -0
  98. package/dist/node/impls/google-calendar.js +3 -0
  99. package/dist/node/impls/gradium-voice.js +3 -0
  100. package/dist/node/impls/granola-meeting-recorder.js +3 -0
  101. package/dist/node/impls/granola-meeting-recorder.mcp.js +3 -0
  102. package/dist/node/impls/health/base-health-provider.js +509 -156
  103. package/dist/node/impls/health/hybrid-health-providers.js +1090 -0
  104. package/dist/node/impls/health/official-health-providers.js +970 -0
  105. package/dist/node/impls/health/provider-normalizers.js +289 -0
  106. package/dist/node/impls/health/providers.js +898 -184
  107. package/dist/node/impls/health-provider-factory.js +1012 -196
  108. package/dist/node/impls/index.js +2589 -259
  109. package/dist/node/impls/jira.js +3 -0
  110. package/dist/node/impls/linear.js +3 -0
  111. package/dist/node/impls/messaging-github.js +112 -0
  112. package/dist/node/impls/messaging-slack.js +82 -0
  113. package/dist/node/impls/messaging-whatsapp-meta.js +54 -0
  114. package/dist/node/impls/messaging-whatsapp-twilio.js +84 -0
  115. package/dist/node/impls/mistral-conversational.js +478 -0
  116. package/dist/node/impls/mistral-conversational.session.js +208 -0
  117. package/dist/node/impls/mistral-embedding.js +3 -0
  118. package/dist/node/impls/mistral-llm.js +3 -0
  119. package/dist/node/impls/mistral-stt.js +169 -0
  120. package/dist/node/impls/notion.js +3 -0
  121. package/dist/node/impls/posthog-reader.js +3 -0
  122. package/dist/node/impls/posthog-utils.js +3 -0
  123. package/dist/node/impls/posthog.js +3 -0
  124. package/dist/node/impls/postmark-email.js +3 -0
  125. package/dist/node/impls/powens-client.js +3 -0
  126. package/dist/node/impls/powens-openbanking.js +3 -0
  127. package/dist/node/impls/provider-factory.js +1986 -249
  128. package/dist/node/impls/qdrant-vector.js +3 -0
  129. package/dist/node/impls/stripe-payments.js +4 -1
  130. package/dist/node/impls/supabase-psql.js +3 -0
  131. package/dist/node/impls/supabase-vector.js +3 -0
  132. package/dist/node/impls/tldv-meeting-recorder.js +3 -0
  133. package/dist/node/impls/twilio-sms.js +3 -0
  134. package/dist/node/index.js +2616 -283
  135. package/dist/node/messaging.js +2 -0
  136. package/dist/node/secrets/provider.js +3 -0
  137. package/dist/secrets/provider.js +2 -0
  138. package/package.json +219 -14
@@ -1,4 +1,286 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
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
+
1
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
+
2
284
  class BaseHealthProvider {
3
285
  providerKey;
4
286
  transport;
@@ -6,146 +288,191 @@ class BaseHealthProvider {
6
288
  mcpUrl;
7
289
  apiKey;
8
290
  accessToken;
291
+ refreshToken;
9
292
  mcpAccessToken;
10
293
  webhookSecret;
294
+ webhookSignatureHeader;
295
+ route;
296
+ aggregatorKey;
297
+ oauth;
11
298
  fetchFn;
12
299
  mcpRequestId = 0;
13
300
  constructor(options) {
14
301
  this.providerKey = options.providerKey;
15
302
  this.transport = options.transport;
16
- this.apiBaseUrl = options.apiBaseUrl ?? "https://api.example-health.local";
303
+ this.apiBaseUrl = options.apiBaseUrl;
17
304
  this.mcpUrl = options.mcpUrl;
18
305
  this.apiKey = options.apiKey;
19
306
  this.accessToken = options.accessToken;
307
+ this.refreshToken = options.oauth?.refreshToken;
20
308
  this.mcpAccessToken = options.mcpAccessToken;
21
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 ?? {};
22
314
  this.fetchFn = options.fetchFn ?? fetch;
23
315
  }
24
- async listActivities(params) {
25
- const result = await this.fetchList("activities", params);
26
- return {
27
- activities: result.items,
28
- nextCursor: result.nextCursor,
29
- hasMore: result.hasMore,
30
- source: this.currentSource()
31
- };
316
+ async listActivities(_params) {
317
+ throw this.unsupported("activities");
32
318
  }
33
- async listWorkouts(params) {
34
- const result = await this.fetchList("workouts", params);
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);
35
364
  return {
36
- workouts: result.items,
37
- nextCursor: result.nextCursor,
38
- hasMore: result.hasMore,
365
+ activities: response.items,
366
+ nextCursor: response.nextCursor,
367
+ hasMore: response.hasMore,
39
368
  source: this.currentSource()
40
369
  };
41
370
  }
42
- async listSleep(params) {
43
- const result = await this.fetchList("sleep", params);
371
+ async fetchWorkouts(params, config) {
372
+ const response = await this.fetchList(params, config);
44
373
  return {
45
- sleep: result.items,
46
- nextCursor: result.nextCursor,
47
- hasMore: result.hasMore,
374
+ workouts: response.items,
375
+ nextCursor: response.nextCursor,
376
+ hasMore: response.hasMore,
48
377
  source: this.currentSource()
49
378
  };
50
379
  }
51
- async listBiometrics(params) {
52
- const result = await this.fetchList("biometrics", params);
380
+ async fetchSleep(params, config) {
381
+ const response = await this.fetchList(params, config);
53
382
  return {
54
- biometrics: result.items,
55
- nextCursor: result.nextCursor,
56
- hasMore: result.hasMore,
383
+ sleep: response.items,
384
+ nextCursor: response.nextCursor,
385
+ hasMore: response.hasMore,
57
386
  source: this.currentSource()
58
387
  };
59
388
  }
60
- async listNutrition(params) {
61
- const result = await this.fetchList("nutrition", params);
389
+ async fetchBiometrics(params, config) {
390
+ const response = await this.fetchList(params, config);
62
391
  return {
63
- nutrition: result.items,
64
- nextCursor: result.nextCursor,
65
- hasMore: result.hasMore,
392
+ biometrics: response.items,
393
+ nextCursor: response.nextCursor,
394
+ hasMore: response.hasMore,
66
395
  source: this.currentSource()
67
396
  };
68
397
  }
69
- async getConnectionStatus(params) {
70
- const payload = await this.fetchRecord("connection/status", params);
71
- const status = readString(payload, "status") ?? "healthy";
398
+ async fetchNutrition(params, config) {
399
+ const response = await this.fetchList(params, config);
72
400
  return {
73
- tenantId: params.tenantId,
74
- connectionId: params.connectionId,
75
- status: status === "healthy" || status === "degraded" || status === "error" || status === "disconnected" ? status : "healthy",
76
- source: this.currentSource(),
77
- lastCheckedAt: readString(payload, "lastCheckedAt") ?? new Date().toISOString(),
78
- errorCode: readString(payload, "errorCode"),
79
- errorMessage: readString(payload, "errorMessage"),
80
- metadata: asRecord(payload.metadata)
401
+ nutrition: response.items,
402
+ nextCursor: response.nextCursor,
403
+ hasMore: response.hasMore,
404
+ source: this.currentSource()
81
405
  };
82
406
  }
83
- async syncActivities(params) {
84
- return this.sync("activities", params);
85
- }
86
- async syncWorkouts(params) {
87
- return this.sync("workouts", params);
407
+ async fetchConnectionStatus(params, config) {
408
+ const payload = await this.fetchPayload(config, params);
409
+ return toHealthConnectionStatus(payload, params, this.currentSource());
88
410
  }
89
- async syncSleep(params) {
90
- return this.sync("sleep", params);
91
- }
92
- async syncBiometrics(params) {
93
- return this.sync("biometrics", params);
94
- }
95
- async syncNutrition(params) {
96
- return this.sync("nutrition", params);
97
- }
98
- async parseWebhook(request) {
99
- const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
100
- const body = asRecord(payload);
411
+ currentSource() {
101
412
  return {
102
413
  providerKey: this.providerKey,
103
- eventType: readString(body, "eventType") ?? readString(body, "event"),
104
- externalEntityId: readString(body, "externalEntityId") ?? readString(body, "entityId"),
105
- entityType: normalizeEntityType(readString(body, "entityType") ?? readString(body, "type")),
106
- receivedAt: new Date().toISOString(),
107
- verified: await this.verifyWebhook(request),
108
- payload
414
+ transport: this.transport,
415
+ route: this.route,
416
+ aggregatorKey: this.aggregatorKey
109
417
  };
110
418
  }
111
- async verifyWebhook(request) {
112
- if (!this.webhookSecret) {
113
- return true;
114
- }
115
- const signature = readHeader(request.headers, "x-webhook-signature");
116
- return signature === this.webhookSecret;
419
+ providerSlug() {
420
+ return this.providerKey.replace("health.", "").replace(/-/g, "_");
421
+ }
422
+ unsupported(capability) {
423
+ return new HealthProviderCapabilityError(`${this.providerKey} does not support ${capability}`);
117
424
  }
118
- async fetchList(resource, params) {
119
- const payload = await this.fetchRecord(resource, params);
120
- const items = asArray(payload.items) ?? asArray(payload[resource]) ?? asArray(payload.records) ?? [];
425
+ async syncFromList(executor) {
426
+ const result = await executor();
427
+ const records = countResultRecords(result);
121
428
  return {
122
- items,
123
- nextCursor: readString(payload, "nextCursor") ?? readString(payload, "cursor"),
124
- hasMore: readBoolean(payload, "hasMore")
429
+ synced: records,
430
+ failed: 0,
431
+ nextCursor: undefined,
432
+ source: result.source
125
433
  };
126
434
  }
127
- async sync(resource, params) {
128
- const payload = await this.fetchRecord(`sync/${resource}`, params, "POST");
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);
129
439
  return {
130
- synced: readNumber(payload, "synced") ?? 0,
131
- failed: readNumber(payload, "failed") ?? 0,
132
- nextCursor: readString(payload, "nextCursor"),
133
- errors: asArray(payload.errors)?.map((item) => String(item)),
134
- source: this.currentSource()
440
+ items,
441
+ nextCursor: pagination.nextCursor,
442
+ hasMore: pagination.hasMore
135
443
  };
136
444
  }
137
- async fetchRecord(resource, params, method = "GET") {
138
- if (this.transport.endsWith("mcp")) {
139
- return this.callMcpTool(resource, params);
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
+ });
140
454
  }
141
- const url = new URL(`${this.apiBaseUrl.replace(/\/$/, "")}/${resource}`);
142
- if (method === "GET") {
143
- for (const [key, value] of Object.entries(params)) {
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)) {
144
470
  if (value == null)
145
471
  continue;
146
472
  if (Array.isArray(value)) {
147
- value.forEach((item) => {
148
- url.searchParams.append(key, String(item));
473
+ value.forEach((entry) => {
474
+ if (entry != null)
475
+ url.searchParams.append(key, String(entry));
149
476
  });
150
477
  continue;
151
478
  }
@@ -154,22 +481,22 @@ class BaseHealthProvider {
154
481
  }
155
482
  const response = await this.fetchFn(url, {
156
483
  method,
157
- headers: {
158
- "Content-Type": "application/json",
159
- ...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
160
- },
161
- body: method === "POST" ? JSON.stringify(params) : undefined
484
+ headers: this.authorizationHeaders(),
485
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
162
486
  });
163
- if (!response.ok) {
164
- const errorBody = await safeResponseText(response);
165
- throw new Error(`${this.providerKey} ${resource} failed (${response.status}): ${errorBody}`);
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);
166
494
  }
167
- const data = await response.json();
168
- return asRecord(data) ?? {};
495
+ return this.readResponsePayload(response, path);
169
496
  }
170
- async callMcpTool(resource, params) {
497
+ async callMcpTool(toolName, args) {
171
498
  if (!this.mcpUrl) {
172
- return {};
499
+ throw new Error(`${this.providerKey} MCP URL is not configured.`);
173
500
  }
174
501
  const response = await this.fetchFn(this.mcpUrl, {
175
502
  method: "POST",
@@ -182,78 +509,103 @@ class BaseHealthProvider {
182
509
  id: ++this.mcpRequestId,
183
510
  method: "tools/call",
184
511
  params: {
185
- name: `${this.providerKey.replace("health.", "")}_${resource.replace(/\//g, "_")}`,
186
- arguments: params
512
+ name: toolName,
513
+ arguments: args
187
514
  }
188
515
  })
189
516
  });
190
- if (!response.ok) {
191
- const errorBody = await safeResponseText(response);
192
- throw new Error(`${this.providerKey} MCP ${resource} failed (${response.status}): ${errorBody}`);
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;
193
524
  }
194
- const rpcPayload = await response.json();
195
- const rpc = asRecord(rpcPayload);
196
- const result = asRecord(rpc?.result) ?? {};
197
- const structured = asRecord(result.structuredContent);
198
- if (structured)
199
- return structured;
200
- const data = asRecord(result.data);
201
- if (data)
202
- return data;
203
- return result;
525
+ return rpcEnvelope.structuredContent ?? rpcEnvelope.data ?? rpcEnvelope;
204
526
  }
205
- currentSource() {
527
+ authorizationHeaders() {
528
+ const token = this.accessToken ?? this.apiKey;
206
529
  return {
207
- providerKey: this.providerKey,
208
- transport: this.transport,
209
- route: "primary"
530
+ "Content-Type": "application/json",
531
+ ...token ? { Authorization: `Bearer ${token}` } : {}
210
532
  };
211
533
  }
212
- }
213
- function safeJsonParse(raw) {
214
- try {
215
- return JSON.parse(raw);
216
- } catch {
217
- return { rawBody: raw };
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();
218
572
  }
219
573
  }
220
574
  function readHeader(headers, key) {
221
- const match = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase());
222
- if (!match)
575
+ const target = key.toLowerCase();
576
+ const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
577
+ if (!entry)
223
578
  return;
224
- const value = match[1];
579
+ const value = entry[1];
225
580
  return Array.isArray(value) ? value[0] : value;
226
581
  }
227
- function normalizeEntityType(value) {
228
- if (!value)
229
- return;
230
- if (value === "activity" || value === "workout" || value === "sleep" || value === "biometric" || value === "nutrition") {
231
- return value;
232
- }
233
- return;
234
- }
235
- function asRecord(value) {
236
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
237
- return;
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
+ }
238
595
  }
239
- return value;
240
- }
241
- function asArray(value) {
242
- return Array.isArray(value) ? value : undefined;
596
+ return 0;
243
597
  }
244
- function readString(record, key) {
245
- const value = record?.[key];
246
- return typeof value === "string" ? value : undefined;
598
+ function ensureTrailingSlash(value) {
599
+ return value.endsWith("/") ? value : `${value}/`;
247
600
  }
248
- function readBoolean(record, key) {
249
- const value = record?.[key];
250
- return typeof value === "boolean" ? value : undefined;
251
- }
252
- function readNumber(record, key) {
253
- const value = record?.[key];
254
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
601
+ function safeJsonParse(raw) {
602
+ try {
603
+ return JSON.parse(raw);
604
+ } catch {
605
+ return { rawBody: raw };
606
+ }
255
607
  }
256
- async function safeResponseText(response) {
608
+ async function safeReadText(response) {
257
609
  try {
258
610
  return await response.text();
259
611
  } catch {
@@ -261,111 +613,473 @@ async function safeResponseText(response) {
261
613
  }
262
614
  }
263
615
 
264
- // src/impls/health/providers.ts
265
- function createProviderOptions(options, fallbackTransport) {
616
+ // src/impls/health/official-health-providers.ts
617
+ function buildSharedQuery(params) {
266
618
  return {
267
- ...options,
268
- transport: options.transport ?? fallbackTransport
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
269
632
  };
270
633
  }
271
634
 
272
635
  class OpenWearablesHealthProvider extends BaseHealthProvider {
636
+ upstreamProvider;
273
637
  constructor(options) {
274
638
  super({
275
- providerKey: "health.openwearables",
276
- ...createProviderOptions(options, "aggregator-api")
639
+ providerKey: options.providerKey ?? "health.openwearables",
640
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.openwearables.io",
641
+ webhookSignatureHeader: "x-openwearables-signature",
642
+ ...options
277
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
+ };
278
704
  }
279
705
  }
280
706
 
281
- class WhoopHealthProvider extends BaseHealthProvider {
707
+ class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
282
708
  constructor(options) {
283
709
  super({
284
- providerKey: "health.whoop",
285
- ...createProviderOptions(options, "official-api")
710
+ ...options,
711
+ providerKey: "health.apple-health",
712
+ upstreamProvider: "apple-health"
286
713
  });
287
714
  }
288
715
  }
289
716
 
290
- class AppleHealthBridgeProvider extends BaseHealthProvider {
717
+ class WhoopHealthProvider extends BaseHealthProvider {
291
718
  constructor(options) {
292
719
  super({
293
- providerKey: "health.apple-health",
294
- ...createProviderOptions(options, "aggregator-api")
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"
295
769
  });
296
770
  }
771
+ context(params) {
772
+ return {
773
+ tenantId: params.tenantId,
774
+ connectionId: params.connectionId,
775
+ providerKey: this.providerKey
776
+ };
777
+ }
297
778
  }
298
779
 
299
780
  class OuraHealthProvider extends BaseHealthProvider {
300
781
  constructor(options) {
301
782
  super({
302
783
  providerKey: "health.oura",
303
- ...createProviderOptions(options, "official-api")
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"
304
832
  });
305
833
  }
834
+ context(params) {
835
+ return {
836
+ tenantId: params.tenantId,
837
+ connectionId: params.connectionId,
838
+ providerKey: this.providerKey
839
+ };
840
+ }
306
841
  }
307
842
 
308
843
  class StravaHealthProvider extends BaseHealthProvider {
309
844
  constructor(options) {
310
845
  super({
311
846
  providerKey: "health.strava",
312
- ...createProviderOptions(options, "official-api")
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
313
854
  });
314
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
+ }
315
894
  }
316
895
 
317
- class GarminHealthProvider extends BaseHealthProvider {
896
+ class FitbitHealthProvider extends BaseHealthProvider {
318
897
  constructor(options) {
319
898
  super({
320
- providerKey: "health.garmin",
321
- ...createProviderOptions(options, "official-api")
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))
322
931
  });
323
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
+ }
324
962
  }
325
963
 
326
- class FitbitHealthProvider extends BaseHealthProvider {
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 {
327
984
  constructor(options) {
328
985
  super({
329
- providerKey: "health.fitbit",
330
- ...createProviderOptions(options, "official-api")
986
+ ...options,
987
+ providerKey: "health.garmin",
988
+ upstreamProvider: "garmin"
331
989
  });
332
990
  }
333
991
  }
334
992
 
335
- class MyFitnessPalHealthProvider extends BaseHealthProvider {
993
+ class MyFitnessPalHealthProvider extends OpenWearablesHealthProvider {
336
994
  constructor(options) {
337
995
  super({
996
+ ...options,
338
997
  providerKey: "health.myfitnesspal",
339
- ...createProviderOptions(options, "official-api")
998
+ upstreamProvider: "myfitnesspal"
340
999
  });
341
1000
  }
342
1001
  }
343
1002
 
344
- class EightSleepHealthProvider extends BaseHealthProvider {
1003
+ class EightSleepHealthProvider extends OpenWearablesHealthProvider {
345
1004
  constructor(options) {
346
1005
  super({
1006
+ ...options,
347
1007
  providerKey: "health.eightsleep",
348
- ...createProviderOptions(options, "official-api")
1008
+ upstreamProvider: "eightsleep"
349
1009
  });
350
1010
  }
351
1011
  }
352
1012
 
353
- class PelotonHealthProvider extends BaseHealthProvider {
1013
+ class PelotonHealthProvider extends OpenWearablesHealthProvider {
354
1014
  constructor(options) {
355
1015
  super({
1016
+ ...options,
356
1017
  providerKey: "health.peloton",
357
- ...createProviderOptions(options, "official-api")
1018
+ upstreamProvider: "peloton"
358
1019
  });
359
1020
  }
360
1021
  }
361
1022
 
362
1023
  class UnofficialHealthAutomationProvider extends BaseHealthProvider {
1024
+ providerSlugValue;
363
1025
  constructor(options) {
364
1026
  super({
365
- ...createProviderOptions(options, "unofficial"),
366
- providerKey: options.providerKey
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")
367
1038
  });
368
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
+ }
369
1083
  }
370
1084
  export {
371
1085
  WhoopHealthProvider,