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