@contractspec/integration.providers-impls 2.8.0 → 2.10.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 +53 -0
- package/dist/health.d.ts +1 -0
- package/dist/health.js +3 -0
- package/dist/impls/health/base-health-provider.d.ts +47 -0
- package/dist/impls/health/base-health-provider.js +266 -0
- package/dist/impls/health/providers.d.ts +39 -0
- package/dist/impls/health/providers.js +383 -0
- package/dist/impls/health-provider-factory.d.ts +3 -0
- package/dist/impls/health-provider-factory.js +495 -0
- package/dist/impls/index.d.ts +2 -0
- package/dist/impls/index.js +512 -4
- package/dist/impls/provider-factory.d.ts +2 -0
- package/dist/impls/provider-factory.js +499 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +515 -4
- package/dist/node/health.js +2 -0
- package/dist/node/impls/health/base-health-provider.js +265 -0
- package/dist/node/impls/health/providers.js +382 -0
- package/dist/node/impls/health-provider-factory.js +494 -0
- package/dist/node/impls/index.js +512 -4
- package/dist/node/impls/provider-factory.js +499 -3
- package/dist/node/index.js +515 -4
- package/package.json +54 -6
package/dist/impls/index.js
CHANGED
|
@@ -2016,6 +2016,498 @@ async function safeReadError3(response) {
|
|
|
2016
2016
|
}
|
|
2017
2017
|
}
|
|
2018
2018
|
|
|
2019
|
+
// src/impls/health/base-health-provider.ts
|
|
2020
|
+
class BaseHealthProvider {
|
|
2021
|
+
providerKey;
|
|
2022
|
+
transport;
|
|
2023
|
+
apiBaseUrl;
|
|
2024
|
+
mcpUrl;
|
|
2025
|
+
apiKey;
|
|
2026
|
+
accessToken;
|
|
2027
|
+
mcpAccessToken;
|
|
2028
|
+
webhookSecret;
|
|
2029
|
+
fetchFn;
|
|
2030
|
+
mcpRequestId = 0;
|
|
2031
|
+
constructor(options) {
|
|
2032
|
+
this.providerKey = options.providerKey;
|
|
2033
|
+
this.transport = options.transport;
|
|
2034
|
+
this.apiBaseUrl = options.apiBaseUrl ?? "https://api.example-health.local";
|
|
2035
|
+
this.mcpUrl = options.mcpUrl;
|
|
2036
|
+
this.apiKey = options.apiKey;
|
|
2037
|
+
this.accessToken = options.accessToken;
|
|
2038
|
+
this.mcpAccessToken = options.mcpAccessToken;
|
|
2039
|
+
this.webhookSecret = options.webhookSecret;
|
|
2040
|
+
this.fetchFn = options.fetchFn ?? fetch;
|
|
2041
|
+
}
|
|
2042
|
+
async listActivities(params) {
|
|
2043
|
+
const result = await this.fetchList("activities", params);
|
|
2044
|
+
return {
|
|
2045
|
+
activities: result.items,
|
|
2046
|
+
nextCursor: result.nextCursor,
|
|
2047
|
+
hasMore: result.hasMore,
|
|
2048
|
+
source: this.currentSource()
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
async listWorkouts(params) {
|
|
2052
|
+
const result = await this.fetchList("workouts", params);
|
|
2053
|
+
return {
|
|
2054
|
+
workouts: result.items,
|
|
2055
|
+
nextCursor: result.nextCursor,
|
|
2056
|
+
hasMore: result.hasMore,
|
|
2057
|
+
source: this.currentSource()
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
async listSleep(params) {
|
|
2061
|
+
const result = await this.fetchList("sleep", params);
|
|
2062
|
+
return {
|
|
2063
|
+
sleep: result.items,
|
|
2064
|
+
nextCursor: result.nextCursor,
|
|
2065
|
+
hasMore: result.hasMore,
|
|
2066
|
+
source: this.currentSource()
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
async listBiometrics(params) {
|
|
2070
|
+
const result = await this.fetchList("biometrics", params);
|
|
2071
|
+
return {
|
|
2072
|
+
biometrics: result.items,
|
|
2073
|
+
nextCursor: result.nextCursor,
|
|
2074
|
+
hasMore: result.hasMore,
|
|
2075
|
+
source: this.currentSource()
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
async listNutrition(params) {
|
|
2079
|
+
const result = await this.fetchList("nutrition", params);
|
|
2080
|
+
return {
|
|
2081
|
+
nutrition: result.items,
|
|
2082
|
+
nextCursor: result.nextCursor,
|
|
2083
|
+
hasMore: result.hasMore,
|
|
2084
|
+
source: this.currentSource()
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
async getConnectionStatus(params) {
|
|
2088
|
+
const payload = await this.fetchRecord("connection/status", params);
|
|
2089
|
+
const status = readString2(payload, "status") ?? "healthy";
|
|
2090
|
+
return {
|
|
2091
|
+
tenantId: params.tenantId,
|
|
2092
|
+
connectionId: params.connectionId,
|
|
2093
|
+
status: status === "healthy" || status === "degraded" || status === "error" || status === "disconnected" ? status : "healthy",
|
|
2094
|
+
source: this.currentSource(),
|
|
2095
|
+
lastCheckedAt: readString2(payload, "lastCheckedAt") ?? new Date().toISOString(),
|
|
2096
|
+
errorCode: readString2(payload, "errorCode"),
|
|
2097
|
+
errorMessage: readString2(payload, "errorMessage"),
|
|
2098
|
+
metadata: asRecord(payload.metadata)
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
async syncActivities(params) {
|
|
2102
|
+
return this.sync("activities", params);
|
|
2103
|
+
}
|
|
2104
|
+
async syncWorkouts(params) {
|
|
2105
|
+
return this.sync("workouts", params);
|
|
2106
|
+
}
|
|
2107
|
+
async syncSleep(params) {
|
|
2108
|
+
return this.sync("sleep", params);
|
|
2109
|
+
}
|
|
2110
|
+
async syncBiometrics(params) {
|
|
2111
|
+
return this.sync("biometrics", params);
|
|
2112
|
+
}
|
|
2113
|
+
async syncNutrition(params) {
|
|
2114
|
+
return this.sync("nutrition", params);
|
|
2115
|
+
}
|
|
2116
|
+
async parseWebhook(request) {
|
|
2117
|
+
const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
|
|
2118
|
+
const body = asRecord(payload);
|
|
2119
|
+
return {
|
|
2120
|
+
providerKey: this.providerKey,
|
|
2121
|
+
eventType: readString2(body, "eventType") ?? readString2(body, "event"),
|
|
2122
|
+
externalEntityId: readString2(body, "externalEntityId") ?? readString2(body, "entityId"),
|
|
2123
|
+
entityType: normalizeEntityType(readString2(body, "entityType") ?? readString2(body, "type")),
|
|
2124
|
+
receivedAt: new Date().toISOString(),
|
|
2125
|
+
verified: await this.verifyWebhook(request),
|
|
2126
|
+
payload
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
async verifyWebhook(request) {
|
|
2130
|
+
if (!this.webhookSecret) {
|
|
2131
|
+
return true;
|
|
2132
|
+
}
|
|
2133
|
+
const signature = readHeader(request.headers, "x-webhook-signature");
|
|
2134
|
+
return signature === this.webhookSecret;
|
|
2135
|
+
}
|
|
2136
|
+
async fetchList(resource, params) {
|
|
2137
|
+
const payload = await this.fetchRecord(resource, params);
|
|
2138
|
+
const items = asArray2(payload.items) ?? asArray2(payload[resource]) ?? asArray2(payload.records) ?? [];
|
|
2139
|
+
return {
|
|
2140
|
+
items,
|
|
2141
|
+
nextCursor: readString2(payload, "nextCursor") ?? readString2(payload, "cursor"),
|
|
2142
|
+
hasMore: readBoolean2(payload, "hasMore")
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
async sync(resource, params) {
|
|
2146
|
+
const payload = await this.fetchRecord(`sync/${resource}`, params, "POST");
|
|
2147
|
+
return {
|
|
2148
|
+
synced: readNumber(payload, "synced") ?? 0,
|
|
2149
|
+
failed: readNumber(payload, "failed") ?? 0,
|
|
2150
|
+
nextCursor: readString2(payload, "nextCursor"),
|
|
2151
|
+
errors: asArray2(payload.errors)?.map((item) => String(item)),
|
|
2152
|
+
source: this.currentSource()
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
async fetchRecord(resource, params, method = "GET") {
|
|
2156
|
+
if (this.transport.endsWith("mcp")) {
|
|
2157
|
+
return this.callMcpTool(resource, params);
|
|
2158
|
+
}
|
|
2159
|
+
const url = new URL(`${this.apiBaseUrl.replace(/\/$/, "")}/${resource}`);
|
|
2160
|
+
if (method === "GET") {
|
|
2161
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2162
|
+
if (value == null)
|
|
2163
|
+
continue;
|
|
2164
|
+
if (Array.isArray(value)) {
|
|
2165
|
+
value.forEach((item) => {
|
|
2166
|
+
url.searchParams.append(key, String(item));
|
|
2167
|
+
});
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
url.searchParams.set(key, String(value));
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
const response = await this.fetchFn(url, {
|
|
2174
|
+
method,
|
|
2175
|
+
headers: {
|
|
2176
|
+
"Content-Type": "application/json",
|
|
2177
|
+
...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
|
|
2178
|
+
},
|
|
2179
|
+
body: method === "POST" ? JSON.stringify(params) : undefined
|
|
2180
|
+
});
|
|
2181
|
+
if (!response.ok) {
|
|
2182
|
+
const errorBody = await safeResponseText(response);
|
|
2183
|
+
throw new Error(`${this.providerKey} ${resource} failed (${response.status}): ${errorBody}`);
|
|
2184
|
+
}
|
|
2185
|
+
const data = await response.json();
|
|
2186
|
+
return asRecord(data) ?? {};
|
|
2187
|
+
}
|
|
2188
|
+
async callMcpTool(resource, params) {
|
|
2189
|
+
if (!this.mcpUrl) {
|
|
2190
|
+
return {};
|
|
2191
|
+
}
|
|
2192
|
+
const response = await this.fetchFn(this.mcpUrl, {
|
|
2193
|
+
method: "POST",
|
|
2194
|
+
headers: {
|
|
2195
|
+
"Content-Type": "application/json",
|
|
2196
|
+
...this.mcpAccessToken ? { Authorization: `Bearer ${this.mcpAccessToken}` } : {}
|
|
2197
|
+
},
|
|
2198
|
+
body: JSON.stringify({
|
|
2199
|
+
jsonrpc: "2.0",
|
|
2200
|
+
id: ++this.mcpRequestId,
|
|
2201
|
+
method: "tools/call",
|
|
2202
|
+
params: {
|
|
2203
|
+
name: `${this.providerKey.replace("health.", "")}_${resource.replace(/\//g, "_")}`,
|
|
2204
|
+
arguments: params
|
|
2205
|
+
}
|
|
2206
|
+
})
|
|
2207
|
+
});
|
|
2208
|
+
if (!response.ok) {
|
|
2209
|
+
const errorBody = await safeResponseText(response);
|
|
2210
|
+
throw new Error(`${this.providerKey} MCP ${resource} failed (${response.status}): ${errorBody}`);
|
|
2211
|
+
}
|
|
2212
|
+
const rpcPayload = await response.json();
|
|
2213
|
+
const rpc = asRecord(rpcPayload);
|
|
2214
|
+
const result = asRecord(rpc?.result) ?? {};
|
|
2215
|
+
const structured = asRecord(result.structuredContent);
|
|
2216
|
+
if (structured)
|
|
2217
|
+
return structured;
|
|
2218
|
+
const data = asRecord(result.data);
|
|
2219
|
+
if (data)
|
|
2220
|
+
return data;
|
|
2221
|
+
return result;
|
|
2222
|
+
}
|
|
2223
|
+
currentSource() {
|
|
2224
|
+
return {
|
|
2225
|
+
providerKey: this.providerKey,
|
|
2226
|
+
transport: this.transport,
|
|
2227
|
+
route: "primary"
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
function safeJsonParse(raw) {
|
|
2232
|
+
try {
|
|
2233
|
+
return JSON.parse(raw);
|
|
2234
|
+
} catch {
|
|
2235
|
+
return { rawBody: raw };
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
function readHeader(headers, key) {
|
|
2239
|
+
const match = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase());
|
|
2240
|
+
if (!match)
|
|
2241
|
+
return;
|
|
2242
|
+
const value = match[1];
|
|
2243
|
+
return Array.isArray(value) ? value[0] : value;
|
|
2244
|
+
}
|
|
2245
|
+
function normalizeEntityType(value) {
|
|
2246
|
+
if (!value)
|
|
2247
|
+
return;
|
|
2248
|
+
if (value === "activity" || value === "workout" || value === "sleep" || value === "biometric" || value === "nutrition") {
|
|
2249
|
+
return value;
|
|
2250
|
+
}
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
function asRecord(value) {
|
|
2254
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
return value;
|
|
2258
|
+
}
|
|
2259
|
+
function asArray2(value) {
|
|
2260
|
+
return Array.isArray(value) ? value : undefined;
|
|
2261
|
+
}
|
|
2262
|
+
function readString2(record, key) {
|
|
2263
|
+
const value = record?.[key];
|
|
2264
|
+
return typeof value === "string" ? value : undefined;
|
|
2265
|
+
}
|
|
2266
|
+
function readBoolean2(record, key) {
|
|
2267
|
+
const value = record?.[key];
|
|
2268
|
+
return typeof value === "boolean" ? value : undefined;
|
|
2269
|
+
}
|
|
2270
|
+
function readNumber(record, key) {
|
|
2271
|
+
const value = record?.[key];
|
|
2272
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2273
|
+
}
|
|
2274
|
+
async function safeResponseText(response) {
|
|
2275
|
+
try {
|
|
2276
|
+
return await response.text();
|
|
2277
|
+
} catch {
|
|
2278
|
+
return response.statusText;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// src/impls/health/providers.ts
|
|
2283
|
+
function createProviderOptions(options, fallbackTransport) {
|
|
2284
|
+
return {
|
|
2285
|
+
...options,
|
|
2286
|
+
transport: options.transport ?? fallbackTransport
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
class OpenWearablesHealthProvider extends BaseHealthProvider {
|
|
2291
|
+
constructor(options) {
|
|
2292
|
+
super({
|
|
2293
|
+
providerKey: "health.openwearables",
|
|
2294
|
+
...createProviderOptions(options, "aggregator-api")
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
class WhoopHealthProvider extends BaseHealthProvider {
|
|
2300
|
+
constructor(options) {
|
|
2301
|
+
super({
|
|
2302
|
+
providerKey: "health.whoop",
|
|
2303
|
+
...createProviderOptions(options, "official-api")
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
class AppleHealthBridgeProvider extends BaseHealthProvider {
|
|
2309
|
+
constructor(options) {
|
|
2310
|
+
super({
|
|
2311
|
+
providerKey: "health.apple-health",
|
|
2312
|
+
...createProviderOptions(options, "aggregator-api")
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
class OuraHealthProvider extends BaseHealthProvider {
|
|
2318
|
+
constructor(options) {
|
|
2319
|
+
super({
|
|
2320
|
+
providerKey: "health.oura",
|
|
2321
|
+
...createProviderOptions(options, "official-api")
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
class StravaHealthProvider extends BaseHealthProvider {
|
|
2327
|
+
constructor(options) {
|
|
2328
|
+
super({
|
|
2329
|
+
providerKey: "health.strava",
|
|
2330
|
+
...createProviderOptions(options, "official-api")
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
class GarminHealthProvider extends BaseHealthProvider {
|
|
2336
|
+
constructor(options) {
|
|
2337
|
+
super({
|
|
2338
|
+
providerKey: "health.garmin",
|
|
2339
|
+
...createProviderOptions(options, "official-api")
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
class FitbitHealthProvider extends BaseHealthProvider {
|
|
2345
|
+
constructor(options) {
|
|
2346
|
+
super({
|
|
2347
|
+
providerKey: "health.fitbit",
|
|
2348
|
+
...createProviderOptions(options, "official-api")
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
class MyFitnessPalHealthProvider extends BaseHealthProvider {
|
|
2354
|
+
constructor(options) {
|
|
2355
|
+
super({
|
|
2356
|
+
providerKey: "health.myfitnesspal",
|
|
2357
|
+
...createProviderOptions(options, "official-api")
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
class EightSleepHealthProvider extends BaseHealthProvider {
|
|
2363
|
+
constructor(options) {
|
|
2364
|
+
super({
|
|
2365
|
+
providerKey: "health.eightsleep",
|
|
2366
|
+
...createProviderOptions(options, "official-api")
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
class PelotonHealthProvider extends BaseHealthProvider {
|
|
2372
|
+
constructor(options) {
|
|
2373
|
+
super({
|
|
2374
|
+
providerKey: "health.peloton",
|
|
2375
|
+
...createProviderOptions(options, "official-api")
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
class UnofficialHealthAutomationProvider extends BaseHealthProvider {
|
|
2381
|
+
constructor(options) {
|
|
2382
|
+
super({
|
|
2383
|
+
...createProviderOptions(options, "unofficial"),
|
|
2384
|
+
providerKey: options.providerKey
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// src/impls/health-provider-factory.ts
|
|
2390
|
+
import {
|
|
2391
|
+
isUnofficialHealthProviderAllowed,
|
|
2392
|
+
resolveHealthStrategyOrder
|
|
2393
|
+
} from "@contractspec/integration.runtime/runtime";
|
|
2394
|
+
function createHealthProviderFromContext(context, secrets) {
|
|
2395
|
+
const providerKey = context.spec.meta.key;
|
|
2396
|
+
const config = toFactoryConfig(context.config);
|
|
2397
|
+
const strategyOrder = buildStrategyOrder(config);
|
|
2398
|
+
const errors = [];
|
|
2399
|
+
for (const strategy of strategyOrder) {
|
|
2400
|
+
const provider = createHealthProviderForStrategy(providerKey, strategy, config, secrets);
|
|
2401
|
+
if (provider) {
|
|
2402
|
+
return provider;
|
|
2403
|
+
}
|
|
2404
|
+
errors.push(`${strategy}: not available`);
|
|
2405
|
+
}
|
|
2406
|
+
throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${errors.join(", ")}.`);
|
|
2407
|
+
}
|
|
2408
|
+
function createHealthProviderForStrategy(providerKey, strategy, config, secrets) {
|
|
2409
|
+
const options = {
|
|
2410
|
+
transport: strategy,
|
|
2411
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
2412
|
+
mcpUrl: config.mcpUrl,
|
|
2413
|
+
apiKey: getSecretString(secrets, "apiKey"),
|
|
2414
|
+
accessToken: getSecretString(secrets, "accessToken"),
|
|
2415
|
+
mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
|
|
2416
|
+
webhookSecret: getSecretString(secrets, "webhookSecret")
|
|
2417
|
+
};
|
|
2418
|
+
if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
|
|
2419
|
+
return new OpenWearablesHealthProvider(options);
|
|
2420
|
+
}
|
|
2421
|
+
if (strategy === "unofficial") {
|
|
2422
|
+
if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
if (providerKey !== "health.myfitnesspal" && providerKey !== "health.eightsleep" && providerKey !== "health.peloton" && providerKey !== "health.garmin") {
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
return new UnofficialHealthAutomationProvider({
|
|
2429
|
+
...options,
|
|
2430
|
+
providerKey
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
if (strategy === "official-mcp") {
|
|
2434
|
+
return createOfficialProvider(providerKey, {
|
|
2435
|
+
...options,
|
|
2436
|
+
transport: "official-mcp"
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
return createOfficialProvider(providerKey, options);
|
|
2440
|
+
}
|
|
2441
|
+
function createOfficialProvider(providerKey, options) {
|
|
2442
|
+
switch (providerKey) {
|
|
2443
|
+
case "health.openwearables":
|
|
2444
|
+
return new OpenWearablesHealthProvider(options);
|
|
2445
|
+
case "health.whoop":
|
|
2446
|
+
return new WhoopHealthProvider(options);
|
|
2447
|
+
case "health.apple-health":
|
|
2448
|
+
return new AppleHealthBridgeProvider(options);
|
|
2449
|
+
case "health.oura":
|
|
2450
|
+
return new OuraHealthProvider(options);
|
|
2451
|
+
case "health.strava":
|
|
2452
|
+
return new StravaHealthProvider(options);
|
|
2453
|
+
case "health.garmin":
|
|
2454
|
+
return new GarminHealthProvider(options);
|
|
2455
|
+
case "health.fitbit":
|
|
2456
|
+
return new FitbitHealthProvider(options);
|
|
2457
|
+
case "health.myfitnesspal":
|
|
2458
|
+
return new MyFitnessPalHealthProvider(options);
|
|
2459
|
+
case "health.eightsleep":
|
|
2460
|
+
return new EightSleepHealthProvider(options);
|
|
2461
|
+
case "health.peloton":
|
|
2462
|
+
return new PelotonHealthProvider(options);
|
|
2463
|
+
default:
|
|
2464
|
+
throw new Error(`Unsupported health provider key: ${providerKey}`);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
function toFactoryConfig(config) {
|
|
2468
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
2469
|
+
return {};
|
|
2470
|
+
}
|
|
2471
|
+
const record = config;
|
|
2472
|
+
return {
|
|
2473
|
+
apiBaseUrl: asString(record.apiBaseUrl),
|
|
2474
|
+
mcpUrl: asString(record.mcpUrl),
|
|
2475
|
+
defaultTransport: normalizeTransport(record.defaultTransport),
|
|
2476
|
+
strategyOrder: normalizeTransportArray(record.strategyOrder),
|
|
2477
|
+
allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
|
|
2478
|
+
unofficialAllowList: Array.isArray(record.unofficialAllowList) ? record.unofficialAllowList.map((item) => typeof item === "string" ? item : undefined).filter((item) => Boolean(item)) : undefined
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
function buildStrategyOrder(config) {
|
|
2482
|
+
const order = resolveHealthStrategyOrder(config);
|
|
2483
|
+
if (!config.defaultTransport) {
|
|
2484
|
+
return order;
|
|
2485
|
+
}
|
|
2486
|
+
const withoutDefault = order.filter((item) => item !== config.defaultTransport);
|
|
2487
|
+
return [config.defaultTransport, ...withoutDefault];
|
|
2488
|
+
}
|
|
2489
|
+
function normalizeTransport(value) {
|
|
2490
|
+
if (typeof value !== "string")
|
|
2491
|
+
return;
|
|
2492
|
+
if (value === "official-api" || value === "official-mcp" || value === "aggregator-api" || value === "aggregator-mcp" || value === "unofficial") {
|
|
2493
|
+
return value;
|
|
2494
|
+
}
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
function normalizeTransportArray(value) {
|
|
2498
|
+
if (!Array.isArray(value))
|
|
2499
|
+
return;
|
|
2500
|
+
const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
|
|
2501
|
+
return transports.length > 0 ? transports : undefined;
|
|
2502
|
+
}
|
|
2503
|
+
function getSecretString(secrets, key) {
|
|
2504
|
+
const value = secrets[key];
|
|
2505
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
2506
|
+
}
|
|
2507
|
+
function asString(value) {
|
|
2508
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2019
2511
|
// src/impls/mistral-llm.ts
|
|
2020
2512
|
import { Mistral } from "@mistralai/mistralai";
|
|
2021
2513
|
|
|
@@ -3389,7 +3881,7 @@ function mapStatus(status) {
|
|
|
3389
3881
|
}
|
|
3390
3882
|
|
|
3391
3883
|
// src/impls/powens-client.ts
|
|
3392
|
-
import { URL } from "url";
|
|
3884
|
+
import { URL as URL2 } from "url";
|
|
3393
3885
|
var POWENS_BASE_URL = {
|
|
3394
3886
|
sandbox: "https://api-sandbox.powens.com/v2",
|
|
3395
3887
|
production: "https://api.powens.com/v2"
|
|
@@ -3475,7 +3967,7 @@ class PowensClient {
|
|
|
3475
3967
|
});
|
|
3476
3968
|
}
|
|
3477
3969
|
async request(options) {
|
|
3478
|
-
const url = new
|
|
3970
|
+
const url = new URL2(options.path, this.baseUrl);
|
|
3479
3971
|
if (options.searchParams) {
|
|
3480
3972
|
for (const [key, value] of Object.entries(options.searchParams)) {
|
|
3481
3973
|
if (value === undefined || value === null)
|
|
@@ -3545,7 +4037,7 @@ class PowensClient {
|
|
|
3545
4037
|
return this.token.accessToken;
|
|
3546
4038
|
}
|
|
3547
4039
|
async fetchAccessToken() {
|
|
3548
|
-
const url = new
|
|
4040
|
+
const url = new URL2("/oauth/token", this.baseUrl);
|
|
3549
4041
|
const basicAuth = Buffer.from(`${this.clientId}:${this.clientSecret}`, "utf-8").toString("base64");
|
|
3550
4042
|
const response = await this.fetchImpl(url, {
|
|
3551
4043
|
method: "POST",
|
|
@@ -4589,6 +5081,10 @@ class IntegrationProviderFactory {
|
|
|
4589
5081
|
throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
|
|
4590
5082
|
}
|
|
4591
5083
|
}
|
|
5084
|
+
async createHealthProvider(context) {
|
|
5085
|
+
const secrets = await this.loadSecrets(context);
|
|
5086
|
+
return createHealthProviderFromContext(context, secrets);
|
|
5087
|
+
}
|
|
4592
5088
|
async loadSecrets(context) {
|
|
4593
5089
|
const cacheKey = context.connection.meta.id;
|
|
4594
5090
|
if (SECRET_CACHE.has(cacheKey)) {
|
|
@@ -4634,11 +5130,15 @@ function requireConfig(context, key, message) {
|
|
|
4634
5130
|
return value;
|
|
4635
5131
|
}
|
|
4636
5132
|
export {
|
|
5133
|
+
createHealthProviderFromContext,
|
|
5134
|
+
WhoopHealthProvider,
|
|
5135
|
+
UnofficialHealthAutomationProvider,
|
|
4637
5136
|
TwilioSmsProvider,
|
|
4638
5137
|
TldvMeetingRecorderProvider,
|
|
4639
5138
|
SupabaseVectorProvider,
|
|
4640
5139
|
SupabasePostgresProvider,
|
|
4641
5140
|
StripePaymentsProvider,
|
|
5141
|
+
StravaHealthProvider,
|
|
4642
5142
|
QdrantVectorProvider,
|
|
4643
5143
|
PowensOpenBankingProvider,
|
|
4644
5144
|
PowensClientError,
|
|
@@ -4646,7 +5146,11 @@ export {
|
|
|
4646
5146
|
PostmarkEmailProvider,
|
|
4647
5147
|
PosthogAnalyticsReader,
|
|
4648
5148
|
PosthogAnalyticsProvider,
|
|
5149
|
+
PelotonHealthProvider,
|
|
5150
|
+
OuraHealthProvider,
|
|
5151
|
+
OpenWearablesHealthProvider,
|
|
4649
5152
|
NotionProjectManagementProvider,
|
|
5153
|
+
MyFitnessPalHealthProvider,
|
|
4650
5154
|
MistralLLMProvider,
|
|
4651
5155
|
MistralEmbeddingProvider,
|
|
4652
5156
|
LinearProjectManagementProvider,
|
|
@@ -4658,8 +5162,12 @@ export {
|
|
|
4658
5162
|
GoogleCalendarProvider,
|
|
4659
5163
|
GmailOutboundProvider,
|
|
4660
5164
|
GmailInboundProvider,
|
|
5165
|
+
GarminHealthProvider,
|
|
5166
|
+
FitbitHealthProvider,
|
|
4661
5167
|
FirefliesMeetingRecorderProvider,
|
|
4662
5168
|
FathomMeetingRecorderProvider,
|
|
4663
5169
|
FalVoiceProvider,
|
|
4664
|
-
ElevenLabsVoiceProvider
|
|
5170
|
+
ElevenLabsVoiceProvider,
|
|
5171
|
+
EightSleepHealthProvider,
|
|
5172
|
+
AppleHealthBridgeProvider
|
|
4665
5173
|
};
|
|
@@ -12,6 +12,7 @@ import type { EmbeddingProvider } from '../embedding';
|
|
|
12
12
|
import type { OpenBankingProvider } from '../openbanking';
|
|
13
13
|
import type { ProjectManagementProvider } from '../project-management';
|
|
14
14
|
import type { MeetingRecorderProvider } from '../meeting-recorder';
|
|
15
|
+
import type { HealthProvider } from '../health';
|
|
15
16
|
export declare class IntegrationProviderFactory {
|
|
16
17
|
createPaymentsProvider(context: IntegrationContext): Promise<PaymentsProvider>;
|
|
17
18
|
createEmailOutboundProvider(context: IntegrationContext): Promise<EmailOutboundProvider>;
|
|
@@ -26,5 +27,6 @@ export declare class IntegrationProviderFactory {
|
|
|
26
27
|
createLlmProvider(context: IntegrationContext): Promise<LLMProvider>;
|
|
27
28
|
createEmbeddingProvider(context: IntegrationContext): Promise<EmbeddingProvider>;
|
|
28
29
|
createOpenBankingProvider(context: IntegrationContext): Promise<OpenBankingProvider>;
|
|
30
|
+
createHealthProvider(context: IntegrationContext): Promise<HealthProvider>;
|
|
29
31
|
private loadSecrets;
|
|
30
32
|
}
|