@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.
- package/README.md +66 -10
- package/dist/impls/async-event-queue.d.ts +8 -0
- package/dist/impls/async-event-queue.js +49 -0
- package/dist/impls/composio-fallback-resolver.d.ts +34 -0
- package/dist/impls/composio-fallback-resolver.js +580 -0
- package/dist/impls/composio-mcp.d.ts +22 -0
- package/dist/impls/composio-mcp.js +164 -0
- package/dist/impls/composio-proxies.d.ts +60 -0
- package/dist/impls/composio-proxies.js +311 -0
- package/dist/impls/composio-sdk.d.ts +25 -0
- package/dist/impls/composio-sdk.js +78 -0
- package/dist/impls/composio-types.d.ts +43 -0
- package/dist/impls/composio-types.js +54 -0
- package/dist/impls/elevenlabs-voice.js +2 -0
- package/dist/impls/fal-voice.js +2 -0
- package/dist/impls/fathom-meeting-recorder.js +2 -0
- package/dist/impls/fathom-meeting-recorder.mapper.js +2 -0
- package/dist/impls/fathom-meeting-recorder.utils.js +2 -0
- package/dist/impls/fathom-meeting-recorder.webhooks.js +2 -0
- package/dist/impls/fireflies-meeting-recorder.js +2 -0
- package/dist/impls/fireflies-meeting-recorder.queries.js +2 -0
- package/dist/impls/fireflies-meeting-recorder.utils.js +2 -0
- package/dist/impls/gcs-storage.js +2 -0
- package/dist/impls/gmail-inbound.js +2 -0
- package/dist/impls/gmail-outbound.js +2 -0
- package/dist/impls/google-calendar.js +2 -0
- package/dist/impls/gradium-voice.js +2 -0
- package/dist/impls/granola-meeting-recorder.js +2 -0
- package/dist/impls/granola-meeting-recorder.mcp.js +2 -0
- package/dist/impls/health/base-health-provider.d.ts +64 -13
- package/dist/impls/health/base-health-provider.js +508 -156
- package/dist/impls/health/hybrid-health-providers.d.ts +34 -0
- package/dist/impls/health/hybrid-health-providers.js +1090 -0
- package/dist/impls/health/official-health-providers.d.ts +78 -0
- package/dist/impls/health/official-health-providers.js +970 -0
- package/dist/impls/health/provider-normalizers.d.ts +28 -0
- package/dist/impls/health/provider-normalizers.js +289 -0
- package/dist/impls/health/providers.d.ts +2 -39
- package/dist/impls/health/providers.js +897 -184
- package/dist/impls/health-provider-factory.js +1011 -196
- package/dist/impls/index.d.ts +11 -0
- package/dist/impls/index.js +2588 -259
- package/dist/impls/jira.js +2 -0
- package/dist/impls/linear.js +2 -0
- package/dist/impls/messaging-github.d.ts +17 -0
- package/dist/impls/messaging-github.js +112 -0
- package/dist/impls/messaging-slack.d.ts +14 -0
- package/dist/impls/messaging-slack.js +82 -0
- package/dist/impls/messaging-whatsapp-meta.d.ts +13 -0
- package/dist/impls/messaging-whatsapp-meta.js +54 -0
- package/dist/impls/messaging-whatsapp-twilio.d.ts +13 -0
- package/dist/impls/messaging-whatsapp-twilio.js +84 -0
- package/dist/impls/mistral-conversational.d.ts +23 -0
- package/dist/impls/mistral-conversational.js +478 -0
- package/dist/impls/mistral-conversational.session.d.ts +32 -0
- package/dist/impls/mistral-conversational.session.js +208 -0
- package/dist/impls/mistral-embedding.js +2 -0
- package/dist/impls/mistral-llm.js +2 -0
- package/dist/impls/mistral-stt.d.ts +17 -0
- package/dist/impls/mistral-stt.js +169 -0
- package/dist/impls/notion.js +2 -0
- package/dist/impls/posthog-reader.js +2 -0
- package/dist/impls/posthog-utils.js +2 -0
- package/dist/impls/posthog.js +2 -0
- package/dist/impls/postmark-email.js +2 -0
- package/dist/impls/powens-client.js +2 -0
- package/dist/impls/powens-openbanking.js +2 -0
- package/dist/impls/provider-factory.d.ts +29 -1
- package/dist/impls/provider-factory.js +1985 -249
- package/dist/impls/qdrant-vector.js +2 -0
- package/dist/impls/stripe-payments.js +3 -1
- package/dist/impls/supabase-psql.js +2 -0
- package/dist/impls/supabase-vector.js +2 -0
- package/dist/impls/tldv-meeting-recorder.js +2 -0
- package/dist/impls/twilio-sms.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2615 -283
- package/dist/messaging.d.ts +1 -0
- package/dist/messaging.js +3 -0
- package/dist/node/impls/async-event-queue.js +49 -0
- package/dist/node/impls/composio-fallback-resolver.js +580 -0
- package/dist/node/impls/composio-mcp.js +164 -0
- package/dist/node/impls/composio-proxies.js +311 -0
- package/dist/node/impls/composio-sdk.js +78 -0
- package/dist/node/impls/composio-types.js +54 -0
- package/dist/node/impls/elevenlabs-voice.js +3 -0
- package/dist/node/impls/fal-voice.js +3 -0
- package/dist/node/impls/fathom-meeting-recorder.js +3 -0
- package/dist/node/impls/fathom-meeting-recorder.mapper.js +3 -0
- package/dist/node/impls/fathom-meeting-recorder.utils.js +3 -0
- package/dist/node/impls/fathom-meeting-recorder.webhooks.js +3 -0
- package/dist/node/impls/fireflies-meeting-recorder.js +3 -0
- package/dist/node/impls/fireflies-meeting-recorder.queries.js +3 -0
- package/dist/node/impls/fireflies-meeting-recorder.utils.js +3 -0
- package/dist/node/impls/gcs-storage.js +3 -0
- package/dist/node/impls/gmail-inbound.js +3 -0
- package/dist/node/impls/gmail-outbound.js +3 -0
- package/dist/node/impls/google-calendar.js +3 -0
- package/dist/node/impls/gradium-voice.js +3 -0
- package/dist/node/impls/granola-meeting-recorder.js +3 -0
- package/dist/node/impls/granola-meeting-recorder.mcp.js +3 -0
- package/dist/node/impls/health/base-health-provider.js +509 -156
- package/dist/node/impls/health/hybrid-health-providers.js +1090 -0
- package/dist/node/impls/health/official-health-providers.js +970 -0
- package/dist/node/impls/health/provider-normalizers.js +289 -0
- package/dist/node/impls/health/providers.js +898 -184
- package/dist/node/impls/health-provider-factory.js +1012 -196
- package/dist/node/impls/index.js +2589 -259
- package/dist/node/impls/jira.js +3 -0
- package/dist/node/impls/linear.js +3 -0
- package/dist/node/impls/messaging-github.js +112 -0
- package/dist/node/impls/messaging-slack.js +82 -0
- package/dist/node/impls/messaging-whatsapp-meta.js +54 -0
- package/dist/node/impls/messaging-whatsapp-twilio.js +84 -0
- package/dist/node/impls/mistral-conversational.js +478 -0
- package/dist/node/impls/mistral-conversational.session.js +208 -0
- package/dist/node/impls/mistral-embedding.js +3 -0
- package/dist/node/impls/mistral-llm.js +3 -0
- package/dist/node/impls/mistral-stt.js +169 -0
- package/dist/node/impls/notion.js +3 -0
- package/dist/node/impls/posthog-reader.js +3 -0
- package/dist/node/impls/posthog-utils.js +3 -0
- package/dist/node/impls/posthog.js +3 -0
- package/dist/node/impls/postmark-email.js +3 -0
- package/dist/node/impls/powens-client.js +3 -0
- package/dist/node/impls/powens-openbanking.js +3 -0
- package/dist/node/impls/provider-factory.js +1986 -249
- package/dist/node/impls/qdrant-vector.js +3 -0
- package/dist/node/impls/stripe-payments.js +4 -1
- package/dist/node/impls/supabase-psql.js +3 -0
- package/dist/node/impls/supabase-vector.js +3 -0
- package/dist/node/impls/tldv-meeting-recorder.js +3 -0
- package/dist/node/impls/twilio-sms.js +3 -0
- package/dist/node/index.js +2616 -283
- package/dist/node/messaging.js +2 -0
- package/dist/node/secrets/provider.js +3 -0
- package/dist/secrets/provider.js +2 -0
- 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
|
|
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(
|
|
25
|
-
|
|
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(
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
nextCursor:
|
|
38
|
-
hasMore:
|
|
365
|
+
activities: response.items,
|
|
366
|
+
nextCursor: response.nextCursor,
|
|
367
|
+
hasMore: response.hasMore,
|
|
39
368
|
source: this.currentSource()
|
|
40
369
|
};
|
|
41
370
|
}
|
|
42
|
-
async
|
|
43
|
-
const
|
|
371
|
+
async fetchWorkouts(params, config) {
|
|
372
|
+
const response = await this.fetchList(params, config);
|
|
44
373
|
return {
|
|
45
|
-
|
|
46
|
-
nextCursor:
|
|
47
|
-
hasMore:
|
|
374
|
+
workouts: response.items,
|
|
375
|
+
nextCursor: response.nextCursor,
|
|
376
|
+
hasMore: response.hasMore,
|
|
48
377
|
source: this.currentSource()
|
|
49
378
|
};
|
|
50
379
|
}
|
|
51
|
-
async
|
|
52
|
-
const
|
|
380
|
+
async fetchSleep(params, config) {
|
|
381
|
+
const response = await this.fetchList(params, config);
|
|
53
382
|
return {
|
|
54
|
-
|
|
55
|
-
nextCursor:
|
|
56
|
-
hasMore:
|
|
383
|
+
sleep: response.items,
|
|
384
|
+
nextCursor: response.nextCursor,
|
|
385
|
+
hasMore: response.hasMore,
|
|
57
386
|
source: this.currentSource()
|
|
58
387
|
};
|
|
59
388
|
}
|
|
60
|
-
async
|
|
61
|
-
const
|
|
389
|
+
async fetchBiometrics(params, config) {
|
|
390
|
+
const response = await this.fetchList(params, config);
|
|
62
391
|
return {
|
|
63
|
-
|
|
64
|
-
nextCursor:
|
|
65
|
-
hasMore:
|
|
392
|
+
biometrics: response.items,
|
|
393
|
+
nextCursor: response.nextCursor,
|
|
394
|
+
hasMore: response.hasMore,
|
|
66
395
|
source: this.currentSource()
|
|
67
396
|
};
|
|
68
397
|
}
|
|
69
|
-
async
|
|
70
|
-
const
|
|
71
|
-
const status = readString(payload, "status") ?? "healthy";
|
|
398
|
+
async fetchNutrition(params, config) {
|
|
399
|
+
const response = await this.fetchList(params, config);
|
|
72
400
|
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
119
|
-
const
|
|
120
|
-
const
|
|
425
|
+
async syncFromList(executor) {
|
|
426
|
+
const result = await executor();
|
|
427
|
+
const records = countResultRecords(result);
|
|
121
428
|
return {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
429
|
+
synced: records,
|
|
430
|
+
failed: 0,
|
|
431
|
+
nextCursor: undefined,
|
|
432
|
+
source: result.source
|
|
125
433
|
};
|
|
126
434
|
}
|
|
127
|
-
async
|
|
128
|
-
const payload = await this.
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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((
|
|
148
|
-
|
|
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
|
-
|
|
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 (
|
|
164
|
-
const
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
return asRecord(data) ?? {};
|
|
495
|
+
return this.readResponsePayload(response, path);
|
|
169
496
|
}
|
|
170
|
-
async callMcpTool(
|
|
497
|
+
async callMcpTool(toolName, args) {
|
|
171
498
|
if (!this.mcpUrl) {
|
|
172
|
-
|
|
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:
|
|
186
|
-
arguments:
|
|
512
|
+
name: toolName,
|
|
513
|
+
arguments: args
|
|
187
514
|
}
|
|
188
515
|
})
|
|
189
516
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
527
|
+
authorizationHeaders() {
|
|
528
|
+
const token = this.accessToken ?? this.apiKey;
|
|
206
529
|
return {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
route: "primary"
|
|
530
|
+
"Content-Type": "application/json",
|
|
531
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
210
532
|
};
|
|
211
533
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
222
|
-
|
|
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 =
|
|
579
|
+
const value = entry[1];
|
|
225
580
|
return Array.isArray(value) ? value[0] : value;
|
|
226
581
|
}
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
240
|
-
}
|
|
241
|
-
function asArray(value) {
|
|
242
|
-
return Array.isArray(value) ? value : undefined;
|
|
596
|
+
return 0;
|
|
243
597
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
return typeof value === "string" ? value : undefined;
|
|
598
|
+
function ensureTrailingSlash(value) {
|
|
599
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
247
600
|
}
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
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
|
|
616
|
+
// src/impls/health/official-health-providers.ts
|
|
617
|
+
function buildSharedQuery(params) {
|
|
266
618
|
return {
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
|
707
|
+
class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
|
|
282
708
|
constructor(options) {
|
|
283
709
|
super({
|
|
284
|
-
|
|
285
|
-
|
|
710
|
+
...options,
|
|
711
|
+
providerKey: "health.apple-health",
|
|
712
|
+
upstreamProvider: "apple-health"
|
|
286
713
|
});
|
|
287
714
|
}
|
|
288
715
|
}
|
|
289
716
|
|
|
290
|
-
class
|
|
717
|
+
class WhoopHealthProvider extends BaseHealthProvider {
|
|
291
718
|
constructor(options) {
|
|
292
719
|
super({
|
|
293
|
-
providerKey: "health.
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
896
|
+
class FitbitHealthProvider extends BaseHealthProvider {
|
|
318
897
|
constructor(options) {
|
|
319
898
|
super({
|
|
320
|
-
providerKey: "health.
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
986
|
+
...options,
|
|
987
|
+
providerKey: "health.garmin",
|
|
988
|
+
upstreamProvider: "garmin"
|
|
331
989
|
});
|
|
332
990
|
}
|
|
333
991
|
}
|
|
334
992
|
|
|
335
|
-
class MyFitnessPalHealthProvider extends
|
|
993
|
+
class MyFitnessPalHealthProvider extends OpenWearablesHealthProvider {
|
|
336
994
|
constructor(options) {
|
|
337
995
|
super({
|
|
996
|
+
...options,
|
|
338
997
|
providerKey: "health.myfitnesspal",
|
|
339
|
-
|
|
998
|
+
upstreamProvider: "myfitnesspal"
|
|
340
999
|
});
|
|
341
1000
|
}
|
|
342
1001
|
}
|
|
343
1002
|
|
|
344
|
-
class EightSleepHealthProvider extends
|
|
1003
|
+
class EightSleepHealthProvider extends OpenWearablesHealthProvider {
|
|
345
1004
|
constructor(options) {
|
|
346
1005
|
super({
|
|
1006
|
+
...options,
|
|
347
1007
|
providerKey: "health.eightsleep",
|
|
348
|
-
|
|
1008
|
+
upstreamProvider: "eightsleep"
|
|
349
1009
|
});
|
|
350
1010
|
}
|
|
351
1011
|
}
|
|
352
1012
|
|
|
353
|
-
class PelotonHealthProvider extends
|
|
1013
|
+
class PelotonHealthProvider extends OpenWearablesHealthProvider {
|
|
354
1014
|
constructor(options) {
|
|
355
1015
|
super({
|
|
1016
|
+
...options,
|
|
356
1017
|
providerKey: "health.peloton",
|
|
357
|
-
|
|
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
|
-
...
|
|
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,
|