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