@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.
@@ -1465,6 +1465,498 @@ async function safeReadError3(response) {
1465
1465
  }
1466
1466
  }
1467
1467
 
1468
+ // src/impls/health/base-health-provider.ts
1469
+ class BaseHealthProvider {
1470
+ providerKey;
1471
+ transport;
1472
+ apiBaseUrl;
1473
+ mcpUrl;
1474
+ apiKey;
1475
+ accessToken;
1476
+ mcpAccessToken;
1477
+ webhookSecret;
1478
+ fetchFn;
1479
+ mcpRequestId = 0;
1480
+ constructor(options) {
1481
+ this.providerKey = options.providerKey;
1482
+ this.transport = options.transport;
1483
+ this.apiBaseUrl = options.apiBaseUrl ?? "https://api.example-health.local";
1484
+ this.mcpUrl = options.mcpUrl;
1485
+ this.apiKey = options.apiKey;
1486
+ this.accessToken = options.accessToken;
1487
+ this.mcpAccessToken = options.mcpAccessToken;
1488
+ this.webhookSecret = options.webhookSecret;
1489
+ this.fetchFn = options.fetchFn ?? fetch;
1490
+ }
1491
+ async listActivities(params) {
1492
+ const result = await this.fetchList("activities", params);
1493
+ return {
1494
+ activities: result.items,
1495
+ nextCursor: result.nextCursor,
1496
+ hasMore: result.hasMore,
1497
+ source: this.currentSource()
1498
+ };
1499
+ }
1500
+ async listWorkouts(params) {
1501
+ const result = await this.fetchList("workouts", params);
1502
+ return {
1503
+ workouts: result.items,
1504
+ nextCursor: result.nextCursor,
1505
+ hasMore: result.hasMore,
1506
+ source: this.currentSource()
1507
+ };
1508
+ }
1509
+ async listSleep(params) {
1510
+ const result = await this.fetchList("sleep", params);
1511
+ return {
1512
+ sleep: result.items,
1513
+ nextCursor: result.nextCursor,
1514
+ hasMore: result.hasMore,
1515
+ source: this.currentSource()
1516
+ };
1517
+ }
1518
+ async listBiometrics(params) {
1519
+ const result = await this.fetchList("biometrics", params);
1520
+ return {
1521
+ biometrics: result.items,
1522
+ nextCursor: result.nextCursor,
1523
+ hasMore: result.hasMore,
1524
+ source: this.currentSource()
1525
+ };
1526
+ }
1527
+ async listNutrition(params) {
1528
+ const result = await this.fetchList("nutrition", params);
1529
+ return {
1530
+ nutrition: result.items,
1531
+ nextCursor: result.nextCursor,
1532
+ hasMore: result.hasMore,
1533
+ source: this.currentSource()
1534
+ };
1535
+ }
1536
+ async getConnectionStatus(params) {
1537
+ const payload = await this.fetchRecord("connection/status", params);
1538
+ const status = readString2(payload, "status") ?? "healthy";
1539
+ return {
1540
+ tenantId: params.tenantId,
1541
+ connectionId: params.connectionId,
1542
+ status: status === "healthy" || status === "degraded" || status === "error" || status === "disconnected" ? status : "healthy",
1543
+ source: this.currentSource(),
1544
+ lastCheckedAt: readString2(payload, "lastCheckedAt") ?? new Date().toISOString(),
1545
+ errorCode: readString2(payload, "errorCode"),
1546
+ errorMessage: readString2(payload, "errorMessage"),
1547
+ metadata: asRecord(payload.metadata)
1548
+ };
1549
+ }
1550
+ async syncActivities(params) {
1551
+ return this.sync("activities", params);
1552
+ }
1553
+ async syncWorkouts(params) {
1554
+ return this.sync("workouts", params);
1555
+ }
1556
+ async syncSleep(params) {
1557
+ return this.sync("sleep", params);
1558
+ }
1559
+ async syncBiometrics(params) {
1560
+ return this.sync("biometrics", params);
1561
+ }
1562
+ async syncNutrition(params) {
1563
+ return this.sync("nutrition", params);
1564
+ }
1565
+ async parseWebhook(request) {
1566
+ const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
1567
+ const body = asRecord(payload);
1568
+ return {
1569
+ providerKey: this.providerKey,
1570
+ eventType: readString2(body, "eventType") ?? readString2(body, "event"),
1571
+ externalEntityId: readString2(body, "externalEntityId") ?? readString2(body, "entityId"),
1572
+ entityType: normalizeEntityType(readString2(body, "entityType") ?? readString2(body, "type")),
1573
+ receivedAt: new Date().toISOString(),
1574
+ verified: await this.verifyWebhook(request),
1575
+ payload
1576
+ };
1577
+ }
1578
+ async verifyWebhook(request) {
1579
+ if (!this.webhookSecret) {
1580
+ return true;
1581
+ }
1582
+ const signature = readHeader(request.headers, "x-webhook-signature");
1583
+ return signature === this.webhookSecret;
1584
+ }
1585
+ async fetchList(resource, params) {
1586
+ const payload = await this.fetchRecord(resource, params);
1587
+ const items = asArray2(payload.items) ?? asArray2(payload[resource]) ?? asArray2(payload.records) ?? [];
1588
+ return {
1589
+ items,
1590
+ nextCursor: readString2(payload, "nextCursor") ?? readString2(payload, "cursor"),
1591
+ hasMore: readBoolean2(payload, "hasMore")
1592
+ };
1593
+ }
1594
+ async sync(resource, params) {
1595
+ const payload = await this.fetchRecord(`sync/${resource}`, params, "POST");
1596
+ return {
1597
+ synced: readNumber(payload, "synced") ?? 0,
1598
+ failed: readNumber(payload, "failed") ?? 0,
1599
+ nextCursor: readString2(payload, "nextCursor"),
1600
+ errors: asArray2(payload.errors)?.map((item) => String(item)),
1601
+ source: this.currentSource()
1602
+ };
1603
+ }
1604
+ async fetchRecord(resource, params, method = "GET") {
1605
+ if (this.transport.endsWith("mcp")) {
1606
+ return this.callMcpTool(resource, params);
1607
+ }
1608
+ const url = new URL(`${this.apiBaseUrl.replace(/\/$/, "")}/${resource}`);
1609
+ if (method === "GET") {
1610
+ for (const [key, value] of Object.entries(params)) {
1611
+ if (value == null)
1612
+ continue;
1613
+ if (Array.isArray(value)) {
1614
+ value.forEach((item) => {
1615
+ url.searchParams.append(key, String(item));
1616
+ });
1617
+ continue;
1618
+ }
1619
+ url.searchParams.set(key, String(value));
1620
+ }
1621
+ }
1622
+ const response = await this.fetchFn(url, {
1623
+ method,
1624
+ headers: {
1625
+ "Content-Type": "application/json",
1626
+ ...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
1627
+ },
1628
+ body: method === "POST" ? JSON.stringify(params) : undefined
1629
+ });
1630
+ if (!response.ok) {
1631
+ const errorBody = await safeResponseText(response);
1632
+ throw new Error(`${this.providerKey} ${resource} failed (${response.status}): ${errorBody}`);
1633
+ }
1634
+ const data = await response.json();
1635
+ return asRecord(data) ?? {};
1636
+ }
1637
+ async callMcpTool(resource, params) {
1638
+ if (!this.mcpUrl) {
1639
+ return {};
1640
+ }
1641
+ const response = await this.fetchFn(this.mcpUrl, {
1642
+ method: "POST",
1643
+ headers: {
1644
+ "Content-Type": "application/json",
1645
+ ...this.mcpAccessToken ? { Authorization: `Bearer ${this.mcpAccessToken}` } : {}
1646
+ },
1647
+ body: JSON.stringify({
1648
+ jsonrpc: "2.0",
1649
+ id: ++this.mcpRequestId,
1650
+ method: "tools/call",
1651
+ params: {
1652
+ name: `${this.providerKey.replace("health.", "")}_${resource.replace(/\//g, "_")}`,
1653
+ arguments: params
1654
+ }
1655
+ })
1656
+ });
1657
+ if (!response.ok) {
1658
+ const errorBody = await safeResponseText(response);
1659
+ throw new Error(`${this.providerKey} MCP ${resource} failed (${response.status}): ${errorBody}`);
1660
+ }
1661
+ const rpcPayload = await response.json();
1662
+ const rpc = asRecord(rpcPayload);
1663
+ const result = asRecord(rpc?.result) ?? {};
1664
+ const structured = asRecord(result.structuredContent);
1665
+ if (structured)
1666
+ return structured;
1667
+ const data = asRecord(result.data);
1668
+ if (data)
1669
+ return data;
1670
+ return result;
1671
+ }
1672
+ currentSource() {
1673
+ return {
1674
+ providerKey: this.providerKey,
1675
+ transport: this.transport,
1676
+ route: "primary"
1677
+ };
1678
+ }
1679
+ }
1680
+ function safeJsonParse(raw) {
1681
+ try {
1682
+ return JSON.parse(raw);
1683
+ } catch {
1684
+ return { rawBody: raw };
1685
+ }
1686
+ }
1687
+ function readHeader(headers, key) {
1688
+ const match = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase());
1689
+ if (!match)
1690
+ return;
1691
+ const value = match[1];
1692
+ return Array.isArray(value) ? value[0] : value;
1693
+ }
1694
+ function normalizeEntityType(value) {
1695
+ if (!value)
1696
+ return;
1697
+ if (value === "activity" || value === "workout" || value === "sleep" || value === "biometric" || value === "nutrition") {
1698
+ return value;
1699
+ }
1700
+ return;
1701
+ }
1702
+ function asRecord(value) {
1703
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1704
+ return;
1705
+ }
1706
+ return value;
1707
+ }
1708
+ function asArray2(value) {
1709
+ return Array.isArray(value) ? value : undefined;
1710
+ }
1711
+ function readString2(record, key) {
1712
+ const value = record?.[key];
1713
+ return typeof value === "string" ? value : undefined;
1714
+ }
1715
+ function readBoolean2(record, key) {
1716
+ const value = record?.[key];
1717
+ return typeof value === "boolean" ? value : undefined;
1718
+ }
1719
+ function readNumber(record, key) {
1720
+ const value = record?.[key];
1721
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1722
+ }
1723
+ async function safeResponseText(response) {
1724
+ try {
1725
+ return await response.text();
1726
+ } catch {
1727
+ return response.statusText;
1728
+ }
1729
+ }
1730
+
1731
+ // src/impls/health/providers.ts
1732
+ function createProviderOptions(options, fallbackTransport) {
1733
+ return {
1734
+ ...options,
1735
+ transport: options.transport ?? fallbackTransport
1736
+ };
1737
+ }
1738
+
1739
+ class OpenWearablesHealthProvider extends BaseHealthProvider {
1740
+ constructor(options) {
1741
+ super({
1742
+ providerKey: "health.openwearables",
1743
+ ...createProviderOptions(options, "aggregator-api")
1744
+ });
1745
+ }
1746
+ }
1747
+
1748
+ class WhoopHealthProvider extends BaseHealthProvider {
1749
+ constructor(options) {
1750
+ super({
1751
+ providerKey: "health.whoop",
1752
+ ...createProviderOptions(options, "official-api")
1753
+ });
1754
+ }
1755
+ }
1756
+
1757
+ class AppleHealthBridgeProvider extends BaseHealthProvider {
1758
+ constructor(options) {
1759
+ super({
1760
+ providerKey: "health.apple-health",
1761
+ ...createProviderOptions(options, "aggregator-api")
1762
+ });
1763
+ }
1764
+ }
1765
+
1766
+ class OuraHealthProvider extends BaseHealthProvider {
1767
+ constructor(options) {
1768
+ super({
1769
+ providerKey: "health.oura",
1770
+ ...createProviderOptions(options, "official-api")
1771
+ });
1772
+ }
1773
+ }
1774
+
1775
+ class StravaHealthProvider extends BaseHealthProvider {
1776
+ constructor(options) {
1777
+ super({
1778
+ providerKey: "health.strava",
1779
+ ...createProviderOptions(options, "official-api")
1780
+ });
1781
+ }
1782
+ }
1783
+
1784
+ class GarminHealthProvider extends BaseHealthProvider {
1785
+ constructor(options) {
1786
+ super({
1787
+ providerKey: "health.garmin",
1788
+ ...createProviderOptions(options, "official-api")
1789
+ });
1790
+ }
1791
+ }
1792
+
1793
+ class FitbitHealthProvider extends BaseHealthProvider {
1794
+ constructor(options) {
1795
+ super({
1796
+ providerKey: "health.fitbit",
1797
+ ...createProviderOptions(options, "official-api")
1798
+ });
1799
+ }
1800
+ }
1801
+
1802
+ class MyFitnessPalHealthProvider extends BaseHealthProvider {
1803
+ constructor(options) {
1804
+ super({
1805
+ providerKey: "health.myfitnesspal",
1806
+ ...createProviderOptions(options, "official-api")
1807
+ });
1808
+ }
1809
+ }
1810
+
1811
+ class EightSleepHealthProvider extends BaseHealthProvider {
1812
+ constructor(options) {
1813
+ super({
1814
+ providerKey: "health.eightsleep",
1815
+ ...createProviderOptions(options, "official-api")
1816
+ });
1817
+ }
1818
+ }
1819
+
1820
+ class PelotonHealthProvider extends BaseHealthProvider {
1821
+ constructor(options) {
1822
+ super({
1823
+ providerKey: "health.peloton",
1824
+ ...createProviderOptions(options, "official-api")
1825
+ });
1826
+ }
1827
+ }
1828
+
1829
+ class UnofficialHealthAutomationProvider extends BaseHealthProvider {
1830
+ constructor(options) {
1831
+ super({
1832
+ ...createProviderOptions(options, "unofficial"),
1833
+ providerKey: options.providerKey
1834
+ });
1835
+ }
1836
+ }
1837
+
1838
+ // src/impls/health-provider-factory.ts
1839
+ import {
1840
+ isUnofficialHealthProviderAllowed,
1841
+ resolveHealthStrategyOrder
1842
+ } from "@contractspec/integration.runtime/runtime";
1843
+ function createHealthProviderFromContext(context, secrets) {
1844
+ const providerKey = context.spec.meta.key;
1845
+ const config = toFactoryConfig(context.config);
1846
+ const strategyOrder = buildStrategyOrder(config);
1847
+ const errors = [];
1848
+ for (const strategy of strategyOrder) {
1849
+ const provider = createHealthProviderForStrategy(providerKey, strategy, config, secrets);
1850
+ if (provider) {
1851
+ return provider;
1852
+ }
1853
+ errors.push(`${strategy}: not available`);
1854
+ }
1855
+ throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${errors.join(", ")}.`);
1856
+ }
1857
+ function createHealthProviderForStrategy(providerKey, strategy, config, secrets) {
1858
+ const options = {
1859
+ transport: strategy,
1860
+ apiBaseUrl: config.apiBaseUrl,
1861
+ mcpUrl: config.mcpUrl,
1862
+ apiKey: getSecretString(secrets, "apiKey"),
1863
+ accessToken: getSecretString(secrets, "accessToken"),
1864
+ mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
1865
+ webhookSecret: getSecretString(secrets, "webhookSecret")
1866
+ };
1867
+ if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
1868
+ return new OpenWearablesHealthProvider(options);
1869
+ }
1870
+ if (strategy === "unofficial") {
1871
+ if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
1872
+ return;
1873
+ }
1874
+ if (providerKey !== "health.myfitnesspal" && providerKey !== "health.eightsleep" && providerKey !== "health.peloton" && providerKey !== "health.garmin") {
1875
+ return;
1876
+ }
1877
+ return new UnofficialHealthAutomationProvider({
1878
+ ...options,
1879
+ providerKey
1880
+ });
1881
+ }
1882
+ if (strategy === "official-mcp") {
1883
+ return createOfficialProvider(providerKey, {
1884
+ ...options,
1885
+ transport: "official-mcp"
1886
+ });
1887
+ }
1888
+ return createOfficialProvider(providerKey, options);
1889
+ }
1890
+ function createOfficialProvider(providerKey, options) {
1891
+ switch (providerKey) {
1892
+ case "health.openwearables":
1893
+ return new OpenWearablesHealthProvider(options);
1894
+ case "health.whoop":
1895
+ return new WhoopHealthProvider(options);
1896
+ case "health.apple-health":
1897
+ return new AppleHealthBridgeProvider(options);
1898
+ case "health.oura":
1899
+ return new OuraHealthProvider(options);
1900
+ case "health.strava":
1901
+ return new StravaHealthProvider(options);
1902
+ case "health.garmin":
1903
+ return new GarminHealthProvider(options);
1904
+ case "health.fitbit":
1905
+ return new FitbitHealthProvider(options);
1906
+ case "health.myfitnesspal":
1907
+ return new MyFitnessPalHealthProvider(options);
1908
+ case "health.eightsleep":
1909
+ return new EightSleepHealthProvider(options);
1910
+ case "health.peloton":
1911
+ return new PelotonHealthProvider(options);
1912
+ default:
1913
+ throw new Error(`Unsupported health provider key: ${providerKey}`);
1914
+ }
1915
+ }
1916
+ function toFactoryConfig(config) {
1917
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
1918
+ return {};
1919
+ }
1920
+ const record = config;
1921
+ return {
1922
+ apiBaseUrl: asString(record.apiBaseUrl),
1923
+ mcpUrl: asString(record.mcpUrl),
1924
+ defaultTransport: normalizeTransport(record.defaultTransport),
1925
+ strategyOrder: normalizeTransportArray(record.strategyOrder),
1926
+ allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
1927
+ unofficialAllowList: Array.isArray(record.unofficialAllowList) ? record.unofficialAllowList.map((item) => typeof item === "string" ? item : undefined).filter((item) => Boolean(item)) : undefined
1928
+ };
1929
+ }
1930
+ function buildStrategyOrder(config) {
1931
+ const order = resolveHealthStrategyOrder(config);
1932
+ if (!config.defaultTransport) {
1933
+ return order;
1934
+ }
1935
+ const withoutDefault = order.filter((item) => item !== config.defaultTransport);
1936
+ return [config.defaultTransport, ...withoutDefault];
1937
+ }
1938
+ function normalizeTransport(value) {
1939
+ if (typeof value !== "string")
1940
+ return;
1941
+ if (value === "official-api" || value === "official-mcp" || value === "aggregator-api" || value === "aggregator-mcp" || value === "unofficial") {
1942
+ return value;
1943
+ }
1944
+ return;
1945
+ }
1946
+ function normalizeTransportArray(value) {
1947
+ if (!Array.isArray(value))
1948
+ return;
1949
+ const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
1950
+ return transports.length > 0 ? transports : undefined;
1951
+ }
1952
+ function getSecretString(secrets, key) {
1953
+ const value = secrets[key];
1954
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
1955
+ }
1956
+ function asString(value) {
1957
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
1958
+ }
1959
+
1468
1960
  // src/impls/mistral-llm.ts
1469
1961
  import { Mistral } from "@mistralai/mistralai";
1470
1962
 
@@ -2838,7 +3330,7 @@ function mapStatus(status) {
2838
3330
  }
2839
3331
 
2840
3332
  // src/impls/powens-client.ts
2841
- import { URL } from "url";
3333
+ import { URL as URL2 } from "url";
2842
3334
  var POWENS_BASE_URL = {
2843
3335
  sandbox: "https://api-sandbox.powens.com/v2",
2844
3336
  production: "https://api.powens.com/v2"
@@ -2924,7 +3416,7 @@ class PowensClient {
2924
3416
  });
2925
3417
  }
2926
3418
  async request(options) {
2927
- const url = new URL(options.path, this.baseUrl);
3419
+ const url = new URL2(options.path, this.baseUrl);
2928
3420
  if (options.searchParams) {
2929
3421
  for (const [key, value] of Object.entries(options.searchParams)) {
2930
3422
  if (value === undefined || value === null)
@@ -2994,7 +3486,7 @@ class PowensClient {
2994
3486
  return this.token.accessToken;
2995
3487
  }
2996
3488
  async fetchAccessToken() {
2997
- const url = new URL("/oauth/token", this.baseUrl);
3489
+ const url = new URL2("/oauth/token", this.baseUrl);
2998
3490
  const basicAuth = Buffer.from(`${this.clientId}:${this.clientSecret}`, "utf-8").toString("base64");
2999
3491
  const response = await this.fetchImpl(url, {
3000
3492
  method: "POST",
@@ -4038,6 +4530,10 @@ class IntegrationProviderFactory {
4038
4530
  throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
4039
4531
  }
4040
4532
  }
4533
+ async createHealthProvider(context) {
4534
+ const secrets = await this.loadSecrets(context);
4535
+ return createHealthProviderFromContext(context, secrets);
4536
+ }
4041
4537
  async loadSecrets(context) {
4042
4538
  const cacheKey = context.connection.meta.id;
4043
4539
  if (SECRET_CACHE.has(cacheKey)) {
package/dist/index.d.ts CHANGED
@@ -13,3 +13,4 @@ export * from './payments';
13
13
  export * from './voice';
14
14
  export * from './project-management';
15
15
  export * from './meeting-recorder';
16
+ export * from './health';