@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,5 +1,286 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// src/impls/health/provider-normalizers.ts
|
|
5
|
+
var DEFAULT_LIST_KEYS = [
|
|
6
|
+
"items",
|
|
7
|
+
"data",
|
|
8
|
+
"records",
|
|
9
|
+
"activities",
|
|
10
|
+
"workouts",
|
|
11
|
+
"sleep",
|
|
12
|
+
"biometrics",
|
|
13
|
+
"nutrition"
|
|
14
|
+
];
|
|
15
|
+
function asRecord(value) {
|
|
16
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function asArray(value) {
|
|
22
|
+
return Array.isArray(value) ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
function readString(record, keys) {
|
|
25
|
+
if (!record)
|
|
26
|
+
return;
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
const value = record[key];
|
|
29
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
function readNumber(record, keys) {
|
|
36
|
+
if (!record)
|
|
37
|
+
return;
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
const value = record[key];
|
|
40
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
44
|
+
const parsed = Number(value);
|
|
45
|
+
if (Number.isFinite(parsed)) {
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
function readBoolean(record, keys) {
|
|
53
|
+
if (!record)
|
|
54
|
+
return;
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
const value = record[key];
|
|
57
|
+
if (typeof value === "boolean") {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
function extractList(payload, listKeys = DEFAULT_LIST_KEYS) {
|
|
64
|
+
const root = asRecord(payload);
|
|
65
|
+
if (!root) {
|
|
66
|
+
return asArray(payload)?.map((item) => asRecord(item)).filter((item) => Boolean(item)) ?? [];
|
|
67
|
+
}
|
|
68
|
+
for (const key of listKeys) {
|
|
69
|
+
const arrayValue = asArray(root[key]);
|
|
70
|
+
if (!arrayValue)
|
|
71
|
+
continue;
|
|
72
|
+
return arrayValue.map((item) => asRecord(item)).filter((item) => Boolean(item));
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
function extractPagination(payload) {
|
|
77
|
+
const root = asRecord(payload);
|
|
78
|
+
const nestedPagination = asRecord(root?.pagination);
|
|
79
|
+
const nextCursor = readString(nestedPagination, ["nextCursor", "next_cursor"]) ?? readString(root, [
|
|
80
|
+
"nextCursor",
|
|
81
|
+
"next_cursor",
|
|
82
|
+
"cursor",
|
|
83
|
+
"next_page_token"
|
|
84
|
+
]);
|
|
85
|
+
const hasMore = readBoolean(nestedPagination, ["hasMore", "has_more"]) ?? readBoolean(root, ["hasMore", "has_more"]);
|
|
86
|
+
return {
|
|
87
|
+
nextCursor,
|
|
88
|
+
hasMore: hasMore ?? Boolean(nextCursor)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function toHealthActivity(item, context, fallbackType = "activity") {
|
|
92
|
+
const externalId = readString(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:${fallbackType}`;
|
|
93
|
+
const id = readString(item, ["id", "uuid", "workout_id"]) ?? `${context.providerKey}:activity:${externalId}`;
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
externalId,
|
|
97
|
+
tenantId: context.tenantId,
|
|
98
|
+
connectionId: context.connectionId ?? "unknown",
|
|
99
|
+
userId: readString(item, ["user_id", "userId", "athlete_id"]),
|
|
100
|
+
providerKey: context.providerKey,
|
|
101
|
+
activityType: readString(item, ["activity_type", "type", "sport_type", "sport"]) ?? fallbackType,
|
|
102
|
+
startedAt: readIsoDate(item, [
|
|
103
|
+
"started_at",
|
|
104
|
+
"start_time",
|
|
105
|
+
"start_date",
|
|
106
|
+
"created_at"
|
|
107
|
+
]),
|
|
108
|
+
endedAt: readIsoDate(item, ["ended_at", "end_time"]),
|
|
109
|
+
durationSeconds: readNumber(item, [
|
|
110
|
+
"duration_seconds",
|
|
111
|
+
"duration",
|
|
112
|
+
"elapsed_time"
|
|
113
|
+
]),
|
|
114
|
+
distanceMeters: readNumber(item, ["distance_meters", "distance"]),
|
|
115
|
+
caloriesKcal: readNumber(item, [
|
|
116
|
+
"calories_kcal",
|
|
117
|
+
"calories",
|
|
118
|
+
"active_kilocalories"
|
|
119
|
+
]),
|
|
120
|
+
steps: readNumber(item, ["steps"])?.valueOf(),
|
|
121
|
+
metadata: item
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function toHealthWorkout(item, context, fallbackType = "workout") {
|
|
125
|
+
const activity = toHealthActivity(item, context, fallbackType);
|
|
126
|
+
return {
|
|
127
|
+
id: activity.id,
|
|
128
|
+
externalId: activity.externalId,
|
|
129
|
+
tenantId: activity.tenantId,
|
|
130
|
+
connectionId: activity.connectionId,
|
|
131
|
+
userId: activity.userId,
|
|
132
|
+
providerKey: activity.providerKey,
|
|
133
|
+
workoutType: readString(item, [
|
|
134
|
+
"workout_type",
|
|
135
|
+
"sport_type",
|
|
136
|
+
"type",
|
|
137
|
+
"activity_type"
|
|
138
|
+
]) ?? fallbackType,
|
|
139
|
+
startedAt: activity.startedAt,
|
|
140
|
+
endedAt: activity.endedAt,
|
|
141
|
+
durationSeconds: activity.durationSeconds,
|
|
142
|
+
distanceMeters: activity.distanceMeters,
|
|
143
|
+
caloriesKcal: activity.caloriesKcal,
|
|
144
|
+
averageHeartRateBpm: readNumber(item, [
|
|
145
|
+
"average_heart_rate",
|
|
146
|
+
"avg_hr",
|
|
147
|
+
"average_heart_rate_bpm"
|
|
148
|
+
]),
|
|
149
|
+
maxHeartRateBpm: readNumber(item, [
|
|
150
|
+
"max_heart_rate",
|
|
151
|
+
"max_hr",
|
|
152
|
+
"max_heart_rate_bpm"
|
|
153
|
+
]),
|
|
154
|
+
metadata: item
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function toHealthSleep(item, context) {
|
|
158
|
+
const externalId = readString(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:sleep`;
|
|
159
|
+
const id = readString(item, ["id", "uuid"]) ?? `${context.providerKey}:sleep:${externalId}`;
|
|
160
|
+
const startedAt = readIsoDate(item, ["started_at", "start_time", "bedtime_start", "start"]) ?? new Date(0).toISOString();
|
|
161
|
+
const endedAt = readIsoDate(item, ["ended_at", "end_time", "bedtime_end", "end"]) ?? startedAt;
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
externalId,
|
|
165
|
+
tenantId: context.tenantId,
|
|
166
|
+
connectionId: context.connectionId ?? "unknown",
|
|
167
|
+
userId: readString(item, ["user_id", "userId"]),
|
|
168
|
+
providerKey: context.providerKey,
|
|
169
|
+
startedAt,
|
|
170
|
+
endedAt,
|
|
171
|
+
durationSeconds: readNumber(item, [
|
|
172
|
+
"duration_seconds",
|
|
173
|
+
"duration",
|
|
174
|
+
"total_sleep_duration"
|
|
175
|
+
]),
|
|
176
|
+
deepSleepSeconds: readNumber(item, [
|
|
177
|
+
"deep_sleep_seconds",
|
|
178
|
+
"deep_sleep_duration"
|
|
179
|
+
]),
|
|
180
|
+
lightSleepSeconds: readNumber(item, [
|
|
181
|
+
"light_sleep_seconds",
|
|
182
|
+
"light_sleep_duration"
|
|
183
|
+
]),
|
|
184
|
+
remSleepSeconds: readNumber(item, [
|
|
185
|
+
"rem_sleep_seconds",
|
|
186
|
+
"rem_sleep_duration"
|
|
187
|
+
]),
|
|
188
|
+
awakeSeconds: readNumber(item, ["awake_seconds", "awake_time"]),
|
|
189
|
+
sleepScore: readNumber(item, ["sleep_score", "score"]),
|
|
190
|
+
metadata: item
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function toHealthBiometric(item, context, metricTypeFallback = "metric") {
|
|
194
|
+
const externalId = readString(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:biometric`;
|
|
195
|
+
const id = readString(item, ["id", "uuid"]) ?? `${context.providerKey}:biometric:${externalId}`;
|
|
196
|
+
return {
|
|
197
|
+
id,
|
|
198
|
+
externalId,
|
|
199
|
+
tenantId: context.tenantId,
|
|
200
|
+
connectionId: context.connectionId ?? "unknown",
|
|
201
|
+
userId: readString(item, ["user_id", "userId"]),
|
|
202
|
+
providerKey: context.providerKey,
|
|
203
|
+
metricType: readString(item, ["metric_type", "metric", "type", "name"]) ?? metricTypeFallback,
|
|
204
|
+
value: readNumber(item, ["value", "score", "measurement"]) ?? 0,
|
|
205
|
+
unit: readString(item, ["unit"]),
|
|
206
|
+
measuredAt: readIsoDate(item, ["measured_at", "timestamp", "created_at"]) ?? new Date().toISOString(),
|
|
207
|
+
metadata: item
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function toHealthNutrition(item, context) {
|
|
211
|
+
const externalId = readString(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:nutrition`;
|
|
212
|
+
const id = readString(item, ["id", "uuid"]) ?? `${context.providerKey}:nutrition:${externalId}`;
|
|
213
|
+
return {
|
|
214
|
+
id,
|
|
215
|
+
externalId,
|
|
216
|
+
tenantId: context.tenantId,
|
|
217
|
+
connectionId: context.connectionId ?? "unknown",
|
|
218
|
+
userId: readString(item, ["user_id", "userId"]),
|
|
219
|
+
providerKey: context.providerKey,
|
|
220
|
+
loggedAt: readIsoDate(item, ["logged_at", "created_at", "date", "timestamp"]) ?? new Date().toISOString(),
|
|
221
|
+
caloriesKcal: readNumber(item, ["calories_kcal", "calories"]),
|
|
222
|
+
proteinGrams: readNumber(item, ["protein_grams", "protein"]),
|
|
223
|
+
carbsGrams: readNumber(item, ["carbs_grams", "carbs"]),
|
|
224
|
+
fatGrams: readNumber(item, ["fat_grams", "fat"]),
|
|
225
|
+
fiberGrams: readNumber(item, ["fiber_grams", "fiber"]),
|
|
226
|
+
hydrationMl: readNumber(item, ["hydration_ml", "water_ml", "water"]),
|
|
227
|
+
metadata: item
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function toHealthConnectionStatus(payload, params, source) {
|
|
231
|
+
const record = asRecord(payload);
|
|
232
|
+
const rawStatus = readString(record, ["status", "connection_status", "health"]) ?? "healthy";
|
|
233
|
+
return {
|
|
234
|
+
tenantId: params.tenantId,
|
|
235
|
+
connectionId: params.connectionId,
|
|
236
|
+
status: rawStatus === "healthy" || rawStatus === "degraded" || rawStatus === "error" || rawStatus === "disconnected" ? rawStatus : "healthy",
|
|
237
|
+
source,
|
|
238
|
+
lastCheckedAt: readIsoDate(record, ["last_checked_at", "lastCheckedAt"]) ?? new Date().toISOString(),
|
|
239
|
+
errorCode: readString(record, ["error_code", "errorCode"]),
|
|
240
|
+
errorMessage: readString(record, ["error_message", "errorMessage"]),
|
|
241
|
+
metadata: asRecord(record?.metadata)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function toHealthWebhookEvent(payload, providerKey, verified) {
|
|
245
|
+
const record = asRecord(payload);
|
|
246
|
+
const entityType = readString(record, ["entity_type", "entityType", "type"]);
|
|
247
|
+
const normalizedEntityType = entityType === "activity" || entityType === "workout" || entityType === "sleep" || entityType === "biometric" || entityType === "nutrition" ? entityType : undefined;
|
|
248
|
+
return {
|
|
249
|
+
providerKey,
|
|
250
|
+
eventType: readString(record, ["event_type", "eventType", "event"]),
|
|
251
|
+
externalEntityId: readString(record, [
|
|
252
|
+
"external_entity_id",
|
|
253
|
+
"externalEntityId",
|
|
254
|
+
"entity_id",
|
|
255
|
+
"entityId",
|
|
256
|
+
"id"
|
|
257
|
+
]),
|
|
258
|
+
entityType: normalizedEntityType,
|
|
259
|
+
receivedAt: new Date().toISOString(),
|
|
260
|
+
verified,
|
|
261
|
+
payload,
|
|
262
|
+
metadata: asRecord(record?.metadata)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function readIsoDate(record, keys) {
|
|
266
|
+
const value = readString(record, keys);
|
|
267
|
+
if (!value)
|
|
268
|
+
return;
|
|
269
|
+
const parsed = new Date(value);
|
|
270
|
+
if (Number.isNaN(parsed.getTime()))
|
|
271
|
+
return;
|
|
272
|
+
return parsed.toISOString();
|
|
273
|
+
}
|
|
274
|
+
|
|
2
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
|
+
|
|
3
284
|
class BaseHealthProvider {
|
|
4
285
|
providerKey;
|
|
5
286
|
transport;
|
|
@@ -7,146 +288,191 @@ class BaseHealthProvider {
|
|
|
7
288
|
mcpUrl;
|
|
8
289
|
apiKey;
|
|
9
290
|
accessToken;
|
|
291
|
+
refreshToken;
|
|
10
292
|
mcpAccessToken;
|
|
11
293
|
webhookSecret;
|
|
294
|
+
webhookSignatureHeader;
|
|
295
|
+
route;
|
|
296
|
+
aggregatorKey;
|
|
297
|
+
oauth;
|
|
12
298
|
fetchFn;
|
|
13
299
|
mcpRequestId = 0;
|
|
14
300
|
constructor(options) {
|
|
15
301
|
this.providerKey = options.providerKey;
|
|
16
302
|
this.transport = options.transport;
|
|
17
|
-
this.apiBaseUrl = options.apiBaseUrl
|
|
303
|
+
this.apiBaseUrl = options.apiBaseUrl;
|
|
18
304
|
this.mcpUrl = options.mcpUrl;
|
|
19
305
|
this.apiKey = options.apiKey;
|
|
20
306
|
this.accessToken = options.accessToken;
|
|
307
|
+
this.refreshToken = options.oauth?.refreshToken;
|
|
21
308
|
this.mcpAccessToken = options.mcpAccessToken;
|
|
22
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 ?? {};
|
|
23
314
|
this.fetchFn = options.fetchFn ?? fetch;
|
|
24
315
|
}
|
|
25
|
-
async listActivities(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
316
|
+
async listActivities(_params) {
|
|
317
|
+
throw this.unsupported("activities");
|
|
318
|
+
}
|
|
319
|
+
async listWorkouts(_params) {
|
|
320
|
+
throw this.unsupported("workouts");
|
|
321
|
+
}
|
|
322
|
+
async listSleep(_params) {
|
|
323
|
+
throw this.unsupported("sleep");
|
|
324
|
+
}
|
|
325
|
+
async listBiometrics(_params) {
|
|
326
|
+
throw this.unsupported("biometrics");
|
|
327
|
+
}
|
|
328
|
+
async listNutrition(_params) {
|
|
329
|
+
throw this.unsupported("nutrition");
|
|
330
|
+
}
|
|
331
|
+
async getConnectionStatus(params) {
|
|
332
|
+
return this.fetchConnectionStatus(params, {
|
|
333
|
+
mcpTool: `${this.providerSlug()}_connection_status`
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async syncActivities(params) {
|
|
337
|
+
return this.syncFromList(() => this.listActivities(params));
|
|
33
338
|
}
|
|
34
|
-
async
|
|
35
|
-
|
|
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);
|
|
36
364
|
return {
|
|
37
|
-
|
|
38
|
-
nextCursor:
|
|
39
|
-
hasMore:
|
|
365
|
+
activities: response.items,
|
|
366
|
+
nextCursor: response.nextCursor,
|
|
367
|
+
hasMore: response.hasMore,
|
|
40
368
|
source: this.currentSource()
|
|
41
369
|
};
|
|
42
370
|
}
|
|
43
|
-
async
|
|
44
|
-
const
|
|
371
|
+
async fetchWorkouts(params, config) {
|
|
372
|
+
const response = await this.fetchList(params, config);
|
|
45
373
|
return {
|
|
46
|
-
|
|
47
|
-
nextCursor:
|
|
48
|
-
hasMore:
|
|
374
|
+
workouts: response.items,
|
|
375
|
+
nextCursor: response.nextCursor,
|
|
376
|
+
hasMore: response.hasMore,
|
|
49
377
|
source: this.currentSource()
|
|
50
378
|
};
|
|
51
379
|
}
|
|
52
|
-
async
|
|
53
|
-
const
|
|
380
|
+
async fetchSleep(params, config) {
|
|
381
|
+
const response = await this.fetchList(params, config);
|
|
54
382
|
return {
|
|
55
|
-
|
|
56
|
-
nextCursor:
|
|
57
|
-
hasMore:
|
|
383
|
+
sleep: response.items,
|
|
384
|
+
nextCursor: response.nextCursor,
|
|
385
|
+
hasMore: response.hasMore,
|
|
58
386
|
source: this.currentSource()
|
|
59
387
|
};
|
|
60
388
|
}
|
|
61
|
-
async
|
|
62
|
-
const
|
|
389
|
+
async fetchBiometrics(params, config) {
|
|
390
|
+
const response = await this.fetchList(params, config);
|
|
63
391
|
return {
|
|
64
|
-
|
|
65
|
-
nextCursor:
|
|
66
|
-
hasMore:
|
|
392
|
+
biometrics: response.items,
|
|
393
|
+
nextCursor: response.nextCursor,
|
|
394
|
+
hasMore: response.hasMore,
|
|
67
395
|
source: this.currentSource()
|
|
68
396
|
};
|
|
69
397
|
}
|
|
70
|
-
async
|
|
71
|
-
const
|
|
72
|
-
const status = readString(payload, "status") ?? "healthy";
|
|
398
|
+
async fetchNutrition(params, config) {
|
|
399
|
+
const response = await this.fetchList(params, config);
|
|
73
400
|
return {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
source: this.currentSource()
|
|
78
|
-
lastCheckedAt: readString(payload, "lastCheckedAt") ?? new Date().toISOString(),
|
|
79
|
-
errorCode: readString(payload, "errorCode"),
|
|
80
|
-
errorMessage: readString(payload, "errorMessage"),
|
|
81
|
-
metadata: asRecord(payload.metadata)
|
|
401
|
+
nutrition: response.items,
|
|
402
|
+
nextCursor: response.nextCursor,
|
|
403
|
+
hasMore: response.hasMore,
|
|
404
|
+
source: this.currentSource()
|
|
82
405
|
};
|
|
83
406
|
}
|
|
84
|
-
async
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
async syncWorkouts(params) {
|
|
88
|
-
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());
|
|
89
410
|
}
|
|
90
|
-
|
|
91
|
-
return this.sync("sleep", params);
|
|
92
|
-
}
|
|
93
|
-
async syncBiometrics(params) {
|
|
94
|
-
return this.sync("biometrics", params);
|
|
95
|
-
}
|
|
96
|
-
async syncNutrition(params) {
|
|
97
|
-
return this.sync("nutrition", params);
|
|
98
|
-
}
|
|
99
|
-
async parseWebhook(request) {
|
|
100
|
-
const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
|
|
101
|
-
const body = asRecord(payload);
|
|
411
|
+
currentSource() {
|
|
102
412
|
return {
|
|
103
413
|
providerKey: this.providerKey,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
receivedAt: new Date().toISOString(),
|
|
108
|
-
verified: await this.verifyWebhook(request),
|
|
109
|
-
payload
|
|
414
|
+
transport: this.transport,
|
|
415
|
+
route: this.route,
|
|
416
|
+
aggregatorKey: this.aggregatorKey
|
|
110
417
|
};
|
|
111
418
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
const signature = readHeader(request.headers, "x-webhook-signature");
|
|
117
|
-
return signature === this.webhookSecret;
|
|
419
|
+
providerSlug() {
|
|
420
|
+
return this.providerKey.replace("health.", "").replace(/-/g, "_");
|
|
118
421
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
422
|
+
unsupported(capability) {
|
|
423
|
+
return new HealthProviderCapabilityError(`${this.providerKey} does not support ${capability}`);
|
|
424
|
+
}
|
|
425
|
+
async syncFromList(executor) {
|
|
426
|
+
const result = await executor();
|
|
427
|
+
const records = countResultRecords(result);
|
|
122
428
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
429
|
+
synced: records,
|
|
430
|
+
failed: 0,
|
|
431
|
+
nextCursor: undefined,
|
|
432
|
+
source: result.source
|
|
126
433
|
};
|
|
127
434
|
}
|
|
128
|
-
async
|
|
129
|
-
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);
|
|
130
439
|
return {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
errors: asArray(payload.errors)?.map((item) => String(item)),
|
|
135
|
-
source: this.currentSource()
|
|
440
|
+
items,
|
|
441
|
+
nextCursor: pagination.nextCursor,
|
|
442
|
+
hasMore: pagination.hasMore
|
|
136
443
|
};
|
|
137
444
|
}
|
|
138
|
-
async
|
|
139
|
-
|
|
140
|
-
|
|
445
|
+
async fetchPayload(config, params) {
|
|
446
|
+
const method = config.method ?? "GET";
|
|
447
|
+
const query = config.buildQuery?.(params);
|
|
448
|
+
const body = config.buildBody?.(params);
|
|
449
|
+
if (this.isMcpTransport()) {
|
|
450
|
+
return this.callMcpTool(config.mcpTool, {
|
|
451
|
+
...query ?? {},
|
|
452
|
+
...body ?? {}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (!config.apiPath || !this.apiBaseUrl) {
|
|
456
|
+
throw new Error(`${this.providerKey} transport is missing an API path.`);
|
|
141
457
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)) {
|
|
145
470
|
if (value == null)
|
|
146
471
|
continue;
|
|
147
472
|
if (Array.isArray(value)) {
|
|
148
|
-
value.forEach((
|
|
149
|
-
|
|
473
|
+
value.forEach((entry) => {
|
|
474
|
+
if (entry != null)
|
|
475
|
+
url.searchParams.append(key, String(entry));
|
|
150
476
|
});
|
|
151
477
|
continue;
|
|
152
478
|
}
|
|
@@ -155,22 +481,22 @@ class BaseHealthProvider {
|
|
|
155
481
|
}
|
|
156
482
|
const response = await this.fetchFn(url, {
|
|
157
483
|
method,
|
|
158
|
-
headers:
|
|
159
|
-
|
|
160
|
-
...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
|
|
161
|
-
},
|
|
162
|
-
body: method === "POST" ? JSON.stringify(params) : undefined
|
|
484
|
+
headers: this.authorizationHeaders(),
|
|
485
|
+
body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
|
|
163
486
|
});
|
|
164
|
-
if (
|
|
165
|
-
const
|
|
166
|
-
|
|
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);
|
|
167
494
|
}
|
|
168
|
-
|
|
169
|
-
return asRecord(data) ?? {};
|
|
495
|
+
return this.readResponsePayload(response, path);
|
|
170
496
|
}
|
|
171
|
-
async callMcpTool(
|
|
497
|
+
async callMcpTool(toolName, args) {
|
|
172
498
|
if (!this.mcpUrl) {
|
|
173
|
-
|
|
499
|
+
throw new Error(`${this.providerKey} MCP URL is not configured.`);
|
|
174
500
|
}
|
|
175
501
|
const response = await this.fetchFn(this.mcpUrl, {
|
|
176
502
|
method: "POST",
|
|
@@ -183,78 +509,103 @@ class BaseHealthProvider {
|
|
|
183
509
|
id: ++this.mcpRequestId,
|
|
184
510
|
method: "tools/call",
|
|
185
511
|
params: {
|
|
186
|
-
name:
|
|
187
|
-
arguments:
|
|
512
|
+
name: toolName,
|
|
513
|
+
arguments: args
|
|
188
514
|
}
|
|
189
515
|
})
|
|
190
516
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
|
194
524
|
}
|
|
195
|
-
|
|
196
|
-
const rpc = asRecord(rpcPayload);
|
|
197
|
-
const result = asRecord(rpc?.result) ?? {};
|
|
198
|
-
const structured = asRecord(result.structuredContent);
|
|
199
|
-
if (structured)
|
|
200
|
-
return structured;
|
|
201
|
-
const data = asRecord(result.data);
|
|
202
|
-
if (data)
|
|
203
|
-
return data;
|
|
204
|
-
return result;
|
|
525
|
+
return rpcEnvelope.structuredContent ?? rpcEnvelope.data ?? rpcEnvelope;
|
|
205
526
|
}
|
|
206
|
-
|
|
527
|
+
authorizationHeaders() {
|
|
528
|
+
const token = this.accessToken ?? this.apiKey;
|
|
207
529
|
return {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
route: "primary"
|
|
530
|
+
"Content-Type": "application/json",
|
|
531
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
211
532
|
};
|
|
212
533
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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();
|
|
219
572
|
}
|
|
220
573
|
}
|
|
221
574
|
function readHeader(headers, key) {
|
|
222
|
-
const
|
|
223
|
-
|
|
575
|
+
const target = key.toLowerCase();
|
|
576
|
+
const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
|
|
577
|
+
if (!entry)
|
|
224
578
|
return;
|
|
225
|
-
const value =
|
|
579
|
+
const value = entry[1];
|
|
226
580
|
return Array.isArray(value) ? value[0] : value;
|
|
227
581
|
}
|
|
228
|
-
function
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
}
|
|
239
595
|
}
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
function asArray(value) {
|
|
243
|
-
return Array.isArray(value) ? value : undefined;
|
|
244
|
-
}
|
|
245
|
-
function readString(record, key) {
|
|
246
|
-
const value = record?.[key];
|
|
247
|
-
return typeof value === "string" ? value : undefined;
|
|
596
|
+
return 0;
|
|
248
597
|
}
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
return typeof value === "boolean" ? value : undefined;
|
|
598
|
+
function ensureTrailingSlash(value) {
|
|
599
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
252
600
|
}
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
|
|
601
|
+
function safeJsonParse(raw) {
|
|
602
|
+
try {
|
|
603
|
+
return JSON.parse(raw);
|
|
604
|
+
} catch {
|
|
605
|
+
return { rawBody: raw };
|
|
606
|
+
}
|
|
256
607
|
}
|
|
257
|
-
async function
|
|
608
|
+
async function safeReadText(response) {
|
|
258
609
|
try {
|
|
259
610
|
return await response.text();
|
|
260
611
|
} catch {
|
|
@@ -262,5 +613,6 @@ async function safeResponseText(response) {
|
|
|
262
613
|
}
|
|
263
614
|
}
|
|
264
615
|
export {
|
|
616
|
+
HealthProviderCapabilityError,
|
|
265
617
|
BaseHealthProvider
|
|
266
618
|
};
|