@contractspec/integration.providers-impls 2.10.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +66 -10
  2. package/dist/impls/async-event-queue.d.ts +8 -0
  3. package/dist/impls/async-event-queue.js +49 -0
  4. package/dist/impls/composio-fallback-resolver.d.ts +34 -0
  5. package/dist/impls/composio-fallback-resolver.js +580 -0
  6. package/dist/impls/composio-mcp.d.ts +22 -0
  7. package/dist/impls/composio-mcp.js +164 -0
  8. package/dist/impls/composio-proxies.d.ts +60 -0
  9. package/dist/impls/composio-proxies.js +311 -0
  10. package/dist/impls/composio-sdk.d.ts +25 -0
  11. package/dist/impls/composio-sdk.js +78 -0
  12. package/dist/impls/composio-types.d.ts +43 -0
  13. package/dist/impls/composio-types.js +54 -0
  14. package/dist/impls/elevenlabs-voice.js +2 -0
  15. package/dist/impls/fal-voice.js +2 -0
  16. package/dist/impls/fathom-meeting-recorder.js +2 -0
  17. package/dist/impls/fathom-meeting-recorder.mapper.js +2 -0
  18. package/dist/impls/fathom-meeting-recorder.utils.js +2 -0
  19. package/dist/impls/fathom-meeting-recorder.webhooks.js +2 -0
  20. package/dist/impls/fireflies-meeting-recorder.js +2 -0
  21. package/dist/impls/fireflies-meeting-recorder.queries.js +2 -0
  22. package/dist/impls/fireflies-meeting-recorder.utils.js +2 -0
  23. package/dist/impls/gcs-storage.js +2 -0
  24. package/dist/impls/gmail-inbound.js +2 -0
  25. package/dist/impls/gmail-outbound.js +2 -0
  26. package/dist/impls/google-calendar.js +2 -0
  27. package/dist/impls/gradium-voice.js +2 -0
  28. package/dist/impls/granola-meeting-recorder.js +2 -0
  29. package/dist/impls/granola-meeting-recorder.mcp.js +2 -0
  30. package/dist/impls/health/base-health-provider.d.ts +64 -13
  31. package/dist/impls/health/base-health-provider.js +508 -156
  32. package/dist/impls/health/hybrid-health-providers.d.ts +34 -0
  33. package/dist/impls/health/hybrid-health-providers.js +1090 -0
  34. package/dist/impls/health/official-health-providers.d.ts +78 -0
  35. package/dist/impls/health/official-health-providers.js +970 -0
  36. package/dist/impls/health/provider-normalizers.d.ts +28 -0
  37. package/dist/impls/health/provider-normalizers.js +289 -0
  38. package/dist/impls/health/providers.d.ts +2 -39
  39. package/dist/impls/health/providers.js +897 -184
  40. package/dist/impls/health-provider-factory.js +1011 -196
  41. package/dist/impls/index.d.ts +11 -0
  42. package/dist/impls/index.js +2588 -259
  43. package/dist/impls/jira.js +2 -0
  44. package/dist/impls/linear.js +2 -0
  45. package/dist/impls/messaging-github.d.ts +17 -0
  46. package/dist/impls/messaging-github.js +112 -0
  47. package/dist/impls/messaging-slack.d.ts +14 -0
  48. package/dist/impls/messaging-slack.js +82 -0
  49. package/dist/impls/messaging-whatsapp-meta.d.ts +13 -0
  50. package/dist/impls/messaging-whatsapp-meta.js +54 -0
  51. package/dist/impls/messaging-whatsapp-twilio.d.ts +13 -0
  52. package/dist/impls/messaging-whatsapp-twilio.js +84 -0
  53. package/dist/impls/mistral-conversational.d.ts +23 -0
  54. package/dist/impls/mistral-conversational.js +478 -0
  55. package/dist/impls/mistral-conversational.session.d.ts +32 -0
  56. package/dist/impls/mistral-conversational.session.js +208 -0
  57. package/dist/impls/mistral-embedding.js +2 -0
  58. package/dist/impls/mistral-llm.js +2 -0
  59. package/dist/impls/mistral-stt.d.ts +17 -0
  60. package/dist/impls/mistral-stt.js +169 -0
  61. package/dist/impls/notion.js +2 -0
  62. package/dist/impls/posthog-reader.js +2 -0
  63. package/dist/impls/posthog-utils.js +2 -0
  64. package/dist/impls/posthog.js +2 -0
  65. package/dist/impls/postmark-email.js +2 -0
  66. package/dist/impls/powens-client.js +2 -0
  67. package/dist/impls/powens-openbanking.js +2 -0
  68. package/dist/impls/provider-factory.d.ts +29 -1
  69. package/dist/impls/provider-factory.js +1985 -249
  70. package/dist/impls/qdrant-vector.js +2 -0
  71. package/dist/impls/stripe-payments.js +3 -1
  72. package/dist/impls/supabase-psql.js +2 -0
  73. package/dist/impls/supabase-vector.js +2 -0
  74. package/dist/impls/tldv-meeting-recorder.js +2 -0
  75. package/dist/impls/twilio-sms.js +2 -0
  76. package/dist/index.d.ts +1 -0
  77. package/dist/index.js +2615 -283
  78. package/dist/messaging.d.ts +1 -0
  79. package/dist/messaging.js +3 -0
  80. package/dist/node/impls/async-event-queue.js +49 -0
  81. package/dist/node/impls/composio-fallback-resolver.js +580 -0
  82. package/dist/node/impls/composio-mcp.js +164 -0
  83. package/dist/node/impls/composio-proxies.js +311 -0
  84. package/dist/node/impls/composio-sdk.js +78 -0
  85. package/dist/node/impls/composio-types.js +54 -0
  86. package/dist/node/impls/elevenlabs-voice.js +3 -0
  87. package/dist/node/impls/fal-voice.js +3 -0
  88. package/dist/node/impls/fathom-meeting-recorder.js +3 -0
  89. package/dist/node/impls/fathom-meeting-recorder.mapper.js +3 -0
  90. package/dist/node/impls/fathom-meeting-recorder.utils.js +3 -0
  91. package/dist/node/impls/fathom-meeting-recorder.webhooks.js +3 -0
  92. package/dist/node/impls/fireflies-meeting-recorder.js +3 -0
  93. package/dist/node/impls/fireflies-meeting-recorder.queries.js +3 -0
  94. package/dist/node/impls/fireflies-meeting-recorder.utils.js +3 -0
  95. package/dist/node/impls/gcs-storage.js +3 -0
  96. package/dist/node/impls/gmail-inbound.js +3 -0
  97. package/dist/node/impls/gmail-outbound.js +3 -0
  98. package/dist/node/impls/google-calendar.js +3 -0
  99. package/dist/node/impls/gradium-voice.js +3 -0
  100. package/dist/node/impls/granola-meeting-recorder.js +3 -0
  101. package/dist/node/impls/granola-meeting-recorder.mcp.js +3 -0
  102. package/dist/node/impls/health/base-health-provider.js +509 -156
  103. package/dist/node/impls/health/hybrid-health-providers.js +1090 -0
  104. package/dist/node/impls/health/official-health-providers.js +970 -0
  105. package/dist/node/impls/health/provider-normalizers.js +289 -0
  106. package/dist/node/impls/health/providers.js +898 -184
  107. package/dist/node/impls/health-provider-factory.js +1012 -196
  108. package/dist/node/impls/index.js +2589 -259
  109. package/dist/node/impls/jira.js +3 -0
  110. package/dist/node/impls/linear.js +3 -0
  111. package/dist/node/impls/messaging-github.js +112 -0
  112. package/dist/node/impls/messaging-slack.js +82 -0
  113. package/dist/node/impls/messaging-whatsapp-meta.js +54 -0
  114. package/dist/node/impls/messaging-whatsapp-twilio.js +84 -0
  115. package/dist/node/impls/mistral-conversational.js +478 -0
  116. package/dist/node/impls/mistral-conversational.session.js +208 -0
  117. package/dist/node/impls/mistral-embedding.js +3 -0
  118. package/dist/node/impls/mistral-llm.js +3 -0
  119. package/dist/node/impls/mistral-stt.js +169 -0
  120. package/dist/node/impls/notion.js +3 -0
  121. package/dist/node/impls/posthog-reader.js +3 -0
  122. package/dist/node/impls/posthog-utils.js +3 -0
  123. package/dist/node/impls/posthog.js +3 -0
  124. package/dist/node/impls/postmark-email.js +3 -0
  125. package/dist/node/impls/powens-client.js +3 -0
  126. package/dist/node/impls/powens-openbanking.js +3 -0
  127. package/dist/node/impls/provider-factory.js +1986 -249
  128. package/dist/node/impls/qdrant-vector.js +3 -0
  129. package/dist/node/impls/stripe-payments.js +4 -1
  130. package/dist/node/impls/supabase-psql.js +3 -0
  131. package/dist/node/impls/supabase-vector.js +3 -0
  132. package/dist/node/impls/tldv-meeting-recorder.js +3 -0
  133. package/dist/node/impls/twilio-sms.js +3 -0
  134. package/dist/node/index.js +2616 -283
  135. package/dist/node/messaging.js +2 -0
  136. package/dist/node/secrets/provider.js +3 -0
  137. package/dist/secrets/provider.js +2 -0
  138. package/package.json +219 -14
@@ -1,4 +1,50 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/impls/async-event-queue.ts
5
+ class AsyncEventQueue {
6
+ values = [];
7
+ waiters = [];
8
+ done = false;
9
+ push(value) {
10
+ if (this.done) {
11
+ return;
12
+ }
13
+ const waiter = this.waiters.shift();
14
+ if (waiter) {
15
+ waiter({ value, done: false });
16
+ return;
17
+ }
18
+ this.values.push(value);
19
+ }
20
+ close() {
21
+ if (this.done) {
22
+ return;
23
+ }
24
+ this.done = true;
25
+ for (const waiter of this.waiters) {
26
+ waiter({ value: undefined, done: true });
27
+ }
28
+ this.waiters.length = 0;
29
+ }
30
+ [Symbol.asyncIterator]() {
31
+ return {
32
+ next: async () => {
33
+ const value = this.values.shift();
34
+ if (value != null) {
35
+ return { value, done: false };
36
+ }
37
+ if (this.done) {
38
+ return { value: undefined, done: true };
39
+ }
40
+ return new Promise((resolve) => {
41
+ this.waiters.push(resolve);
42
+ });
43
+ }
44
+ };
45
+ }
46
+ }
47
+
2
48
  // src/impls/elevenlabs-voice.ts
3
49
  import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
4
50
  var FORMAT_MAP = {
@@ -1465,7 +1511,286 @@ async function safeReadError3(response) {
1465
1511
  }
1466
1512
  }
1467
1513
 
1514
+ // src/impls/health/provider-normalizers.ts
1515
+ var DEFAULT_LIST_KEYS = [
1516
+ "items",
1517
+ "data",
1518
+ "records",
1519
+ "activities",
1520
+ "workouts",
1521
+ "sleep",
1522
+ "biometrics",
1523
+ "nutrition"
1524
+ ];
1525
+ function asRecord(value) {
1526
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1527
+ return;
1528
+ }
1529
+ return value;
1530
+ }
1531
+ function asArray2(value) {
1532
+ return Array.isArray(value) ? value : undefined;
1533
+ }
1534
+ function readString2(record, keys) {
1535
+ if (!record)
1536
+ return;
1537
+ for (const key of keys) {
1538
+ const value = record[key];
1539
+ if (typeof value === "string" && value.trim().length > 0) {
1540
+ return value;
1541
+ }
1542
+ }
1543
+ return;
1544
+ }
1545
+ function readNumber(record, keys) {
1546
+ if (!record)
1547
+ return;
1548
+ for (const key of keys) {
1549
+ const value = record[key];
1550
+ if (typeof value === "number" && Number.isFinite(value)) {
1551
+ return value;
1552
+ }
1553
+ if (typeof value === "string" && value.trim().length > 0) {
1554
+ const parsed = Number(value);
1555
+ if (Number.isFinite(parsed)) {
1556
+ return parsed;
1557
+ }
1558
+ }
1559
+ }
1560
+ return;
1561
+ }
1562
+ function readBoolean2(record, keys) {
1563
+ if (!record)
1564
+ return;
1565
+ for (const key of keys) {
1566
+ const value = record[key];
1567
+ if (typeof value === "boolean") {
1568
+ return value;
1569
+ }
1570
+ }
1571
+ return;
1572
+ }
1573
+ function extractList(payload, listKeys = DEFAULT_LIST_KEYS) {
1574
+ const root = asRecord(payload);
1575
+ if (!root) {
1576
+ return asArray2(payload)?.map((item) => asRecord(item)).filter((item) => Boolean(item)) ?? [];
1577
+ }
1578
+ for (const key of listKeys) {
1579
+ const arrayValue = asArray2(root[key]);
1580
+ if (!arrayValue)
1581
+ continue;
1582
+ return arrayValue.map((item) => asRecord(item)).filter((item) => Boolean(item));
1583
+ }
1584
+ return [];
1585
+ }
1586
+ function extractPagination(payload) {
1587
+ const root = asRecord(payload);
1588
+ const nestedPagination = asRecord(root?.pagination);
1589
+ const nextCursor = readString2(nestedPagination, ["nextCursor", "next_cursor"]) ?? readString2(root, [
1590
+ "nextCursor",
1591
+ "next_cursor",
1592
+ "cursor",
1593
+ "next_page_token"
1594
+ ]);
1595
+ const hasMore = readBoolean2(nestedPagination, ["hasMore", "has_more"]) ?? readBoolean2(root, ["hasMore", "has_more"]);
1596
+ return {
1597
+ nextCursor,
1598
+ hasMore: hasMore ?? Boolean(nextCursor)
1599
+ };
1600
+ }
1601
+ function toHealthActivity(item, context, fallbackType = "activity") {
1602
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:${fallbackType}`;
1603
+ const id = readString2(item, ["id", "uuid", "workout_id"]) ?? `${context.providerKey}:activity:${externalId}`;
1604
+ return {
1605
+ id,
1606
+ externalId,
1607
+ tenantId: context.tenantId,
1608
+ connectionId: context.connectionId ?? "unknown",
1609
+ userId: readString2(item, ["user_id", "userId", "athlete_id"]),
1610
+ providerKey: context.providerKey,
1611
+ activityType: readString2(item, ["activity_type", "type", "sport_type", "sport"]) ?? fallbackType,
1612
+ startedAt: readIsoDate(item, [
1613
+ "started_at",
1614
+ "start_time",
1615
+ "start_date",
1616
+ "created_at"
1617
+ ]),
1618
+ endedAt: readIsoDate(item, ["ended_at", "end_time"]),
1619
+ durationSeconds: readNumber(item, [
1620
+ "duration_seconds",
1621
+ "duration",
1622
+ "elapsed_time"
1623
+ ]),
1624
+ distanceMeters: readNumber(item, ["distance_meters", "distance"]),
1625
+ caloriesKcal: readNumber(item, [
1626
+ "calories_kcal",
1627
+ "calories",
1628
+ "active_kilocalories"
1629
+ ]),
1630
+ steps: readNumber(item, ["steps"])?.valueOf(),
1631
+ metadata: item
1632
+ };
1633
+ }
1634
+ function toHealthWorkout(item, context, fallbackType = "workout") {
1635
+ const activity = toHealthActivity(item, context, fallbackType);
1636
+ return {
1637
+ id: activity.id,
1638
+ externalId: activity.externalId,
1639
+ tenantId: activity.tenantId,
1640
+ connectionId: activity.connectionId,
1641
+ userId: activity.userId,
1642
+ providerKey: activity.providerKey,
1643
+ workoutType: readString2(item, [
1644
+ "workout_type",
1645
+ "sport_type",
1646
+ "type",
1647
+ "activity_type"
1648
+ ]) ?? fallbackType,
1649
+ startedAt: activity.startedAt,
1650
+ endedAt: activity.endedAt,
1651
+ durationSeconds: activity.durationSeconds,
1652
+ distanceMeters: activity.distanceMeters,
1653
+ caloriesKcal: activity.caloriesKcal,
1654
+ averageHeartRateBpm: readNumber(item, [
1655
+ "average_heart_rate",
1656
+ "avg_hr",
1657
+ "average_heart_rate_bpm"
1658
+ ]),
1659
+ maxHeartRateBpm: readNumber(item, [
1660
+ "max_heart_rate",
1661
+ "max_hr",
1662
+ "max_heart_rate_bpm"
1663
+ ]),
1664
+ metadata: item
1665
+ };
1666
+ }
1667
+ function toHealthSleep(item, context) {
1668
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:sleep`;
1669
+ const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:sleep:${externalId}`;
1670
+ const startedAt = readIsoDate(item, ["started_at", "start_time", "bedtime_start", "start"]) ?? new Date(0).toISOString();
1671
+ const endedAt = readIsoDate(item, ["ended_at", "end_time", "bedtime_end", "end"]) ?? startedAt;
1672
+ return {
1673
+ id,
1674
+ externalId,
1675
+ tenantId: context.tenantId,
1676
+ connectionId: context.connectionId ?? "unknown",
1677
+ userId: readString2(item, ["user_id", "userId"]),
1678
+ providerKey: context.providerKey,
1679
+ startedAt,
1680
+ endedAt,
1681
+ durationSeconds: readNumber(item, [
1682
+ "duration_seconds",
1683
+ "duration",
1684
+ "total_sleep_duration"
1685
+ ]),
1686
+ deepSleepSeconds: readNumber(item, [
1687
+ "deep_sleep_seconds",
1688
+ "deep_sleep_duration"
1689
+ ]),
1690
+ lightSleepSeconds: readNumber(item, [
1691
+ "light_sleep_seconds",
1692
+ "light_sleep_duration"
1693
+ ]),
1694
+ remSleepSeconds: readNumber(item, [
1695
+ "rem_sleep_seconds",
1696
+ "rem_sleep_duration"
1697
+ ]),
1698
+ awakeSeconds: readNumber(item, ["awake_seconds", "awake_time"]),
1699
+ sleepScore: readNumber(item, ["sleep_score", "score"]),
1700
+ metadata: item
1701
+ };
1702
+ }
1703
+ function toHealthBiometric(item, context, metricTypeFallback = "metric") {
1704
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:biometric`;
1705
+ const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:biometric:${externalId}`;
1706
+ return {
1707
+ id,
1708
+ externalId,
1709
+ tenantId: context.tenantId,
1710
+ connectionId: context.connectionId ?? "unknown",
1711
+ userId: readString2(item, ["user_id", "userId"]),
1712
+ providerKey: context.providerKey,
1713
+ metricType: readString2(item, ["metric_type", "metric", "type", "name"]) ?? metricTypeFallback,
1714
+ value: readNumber(item, ["value", "score", "measurement"]) ?? 0,
1715
+ unit: readString2(item, ["unit"]),
1716
+ measuredAt: readIsoDate(item, ["measured_at", "timestamp", "created_at"]) ?? new Date().toISOString(),
1717
+ metadata: item
1718
+ };
1719
+ }
1720
+ function toHealthNutrition(item, context) {
1721
+ const externalId = readString2(item, ["external_id", "externalId", "uuid", "id"]) ?? `${context.providerKey}:nutrition`;
1722
+ const id = readString2(item, ["id", "uuid"]) ?? `${context.providerKey}:nutrition:${externalId}`;
1723
+ return {
1724
+ id,
1725
+ externalId,
1726
+ tenantId: context.tenantId,
1727
+ connectionId: context.connectionId ?? "unknown",
1728
+ userId: readString2(item, ["user_id", "userId"]),
1729
+ providerKey: context.providerKey,
1730
+ loggedAt: readIsoDate(item, ["logged_at", "created_at", "date", "timestamp"]) ?? new Date().toISOString(),
1731
+ caloriesKcal: readNumber(item, ["calories_kcal", "calories"]),
1732
+ proteinGrams: readNumber(item, ["protein_grams", "protein"]),
1733
+ carbsGrams: readNumber(item, ["carbs_grams", "carbs"]),
1734
+ fatGrams: readNumber(item, ["fat_grams", "fat"]),
1735
+ fiberGrams: readNumber(item, ["fiber_grams", "fiber"]),
1736
+ hydrationMl: readNumber(item, ["hydration_ml", "water_ml", "water"]),
1737
+ metadata: item
1738
+ };
1739
+ }
1740
+ function toHealthConnectionStatus(payload, params, source) {
1741
+ const record = asRecord(payload);
1742
+ const rawStatus = readString2(record, ["status", "connection_status", "health"]) ?? "healthy";
1743
+ return {
1744
+ tenantId: params.tenantId,
1745
+ connectionId: params.connectionId,
1746
+ status: rawStatus === "healthy" || rawStatus === "degraded" || rawStatus === "error" || rawStatus === "disconnected" ? rawStatus : "healthy",
1747
+ source,
1748
+ lastCheckedAt: readIsoDate(record, ["last_checked_at", "lastCheckedAt"]) ?? new Date().toISOString(),
1749
+ errorCode: readString2(record, ["error_code", "errorCode"]),
1750
+ errorMessage: readString2(record, ["error_message", "errorMessage"]),
1751
+ metadata: asRecord(record?.metadata)
1752
+ };
1753
+ }
1754
+ function toHealthWebhookEvent(payload, providerKey, verified) {
1755
+ const record = asRecord(payload);
1756
+ const entityType = readString2(record, ["entity_type", "entityType", "type"]);
1757
+ const normalizedEntityType = entityType === "activity" || entityType === "workout" || entityType === "sleep" || entityType === "biometric" || entityType === "nutrition" ? entityType : undefined;
1758
+ return {
1759
+ providerKey,
1760
+ eventType: readString2(record, ["event_type", "eventType", "event"]),
1761
+ externalEntityId: readString2(record, [
1762
+ "external_entity_id",
1763
+ "externalEntityId",
1764
+ "entity_id",
1765
+ "entityId",
1766
+ "id"
1767
+ ]),
1768
+ entityType: normalizedEntityType,
1769
+ receivedAt: new Date().toISOString(),
1770
+ verified,
1771
+ payload,
1772
+ metadata: asRecord(record?.metadata)
1773
+ };
1774
+ }
1775
+ function readIsoDate(record, keys) {
1776
+ const value = readString2(record, keys);
1777
+ if (!value)
1778
+ return;
1779
+ const parsed = new Date(value);
1780
+ if (Number.isNaN(parsed.getTime()))
1781
+ return;
1782
+ return parsed.toISOString();
1783
+ }
1784
+
1468
1785
  // src/impls/health/base-health-provider.ts
1786
+ class HealthProviderCapabilityError extends Error {
1787
+ code = "NOT_SUPPORTED";
1788
+ constructor(message) {
1789
+ super(message);
1790
+ this.name = "HealthProviderCapabilityError";
1791
+ }
1792
+ }
1793
+
1469
1794
  class BaseHealthProvider {
1470
1795
  providerKey;
1471
1796
  transport;
@@ -1473,146 +1798,191 @@ class BaseHealthProvider {
1473
1798
  mcpUrl;
1474
1799
  apiKey;
1475
1800
  accessToken;
1801
+ refreshToken;
1476
1802
  mcpAccessToken;
1477
1803
  webhookSecret;
1804
+ webhookSignatureHeader;
1805
+ route;
1806
+ aggregatorKey;
1807
+ oauth;
1478
1808
  fetchFn;
1479
1809
  mcpRequestId = 0;
1480
1810
  constructor(options) {
1481
1811
  this.providerKey = options.providerKey;
1482
1812
  this.transport = options.transport;
1483
- this.apiBaseUrl = options.apiBaseUrl ?? "https://api.example-health.local";
1813
+ this.apiBaseUrl = options.apiBaseUrl;
1484
1814
  this.mcpUrl = options.mcpUrl;
1485
1815
  this.apiKey = options.apiKey;
1486
1816
  this.accessToken = options.accessToken;
1817
+ this.refreshToken = options.oauth?.refreshToken;
1487
1818
  this.mcpAccessToken = options.mcpAccessToken;
1488
1819
  this.webhookSecret = options.webhookSecret;
1820
+ this.webhookSignatureHeader = options.webhookSignatureHeader ?? "x-webhook-signature";
1821
+ this.route = options.route ?? "primary";
1822
+ this.aggregatorKey = options.aggregatorKey;
1823
+ this.oauth = options.oauth ?? {};
1489
1824
  this.fetchFn = options.fetchFn ?? fetch;
1490
1825
  }
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
- };
1826
+ async listActivities(_params) {
1827
+ throw this.unsupported("activities");
1499
1828
  }
1500
- async listWorkouts(params) {
1501
- const result = await this.fetchList("workouts", params);
1829
+ async listWorkouts(_params) {
1830
+ throw this.unsupported("workouts");
1831
+ }
1832
+ async listSleep(_params) {
1833
+ throw this.unsupported("sleep");
1834
+ }
1835
+ async listBiometrics(_params) {
1836
+ throw this.unsupported("biometrics");
1837
+ }
1838
+ async listNutrition(_params) {
1839
+ throw this.unsupported("nutrition");
1840
+ }
1841
+ async getConnectionStatus(params) {
1842
+ return this.fetchConnectionStatus(params, {
1843
+ mcpTool: `${this.providerSlug()}_connection_status`
1844
+ });
1845
+ }
1846
+ async syncActivities(params) {
1847
+ return this.syncFromList(() => this.listActivities(params));
1848
+ }
1849
+ async syncWorkouts(params) {
1850
+ return this.syncFromList(() => this.listWorkouts(params));
1851
+ }
1852
+ async syncSleep(params) {
1853
+ return this.syncFromList(() => this.listSleep(params));
1854
+ }
1855
+ async syncBiometrics(params) {
1856
+ return this.syncFromList(() => this.listBiometrics(params));
1857
+ }
1858
+ async syncNutrition(params) {
1859
+ return this.syncFromList(() => this.listNutrition(params));
1860
+ }
1861
+ async parseWebhook(request) {
1862
+ const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
1863
+ const verified = await this.verifyWebhook(request);
1864
+ return toHealthWebhookEvent(payload, this.providerKey, verified);
1865
+ }
1866
+ async verifyWebhook(request) {
1867
+ if (!this.webhookSecret)
1868
+ return true;
1869
+ const signature = readHeader(request.headers, this.webhookSignatureHeader);
1870
+ return signature === this.webhookSecret;
1871
+ }
1872
+ async fetchActivities(params, config) {
1873
+ const response = await this.fetchList(params, config);
1502
1874
  return {
1503
- workouts: result.items,
1504
- nextCursor: result.nextCursor,
1505
- hasMore: result.hasMore,
1875
+ activities: response.items,
1876
+ nextCursor: response.nextCursor,
1877
+ hasMore: response.hasMore,
1506
1878
  source: this.currentSource()
1507
1879
  };
1508
1880
  }
1509
- async listSleep(params) {
1510
- const result = await this.fetchList("sleep", params);
1881
+ async fetchWorkouts(params, config) {
1882
+ const response = await this.fetchList(params, config);
1511
1883
  return {
1512
- sleep: result.items,
1513
- nextCursor: result.nextCursor,
1514
- hasMore: result.hasMore,
1884
+ workouts: response.items,
1885
+ nextCursor: response.nextCursor,
1886
+ hasMore: response.hasMore,
1515
1887
  source: this.currentSource()
1516
1888
  };
1517
1889
  }
1518
- async listBiometrics(params) {
1519
- const result = await this.fetchList("biometrics", params);
1890
+ async fetchSleep(params, config) {
1891
+ const response = await this.fetchList(params, config);
1520
1892
  return {
1521
- biometrics: result.items,
1522
- nextCursor: result.nextCursor,
1523
- hasMore: result.hasMore,
1893
+ sleep: response.items,
1894
+ nextCursor: response.nextCursor,
1895
+ hasMore: response.hasMore,
1524
1896
  source: this.currentSource()
1525
1897
  };
1526
1898
  }
1527
- async listNutrition(params) {
1528
- const result = await this.fetchList("nutrition", params);
1899
+ async fetchBiometrics(params, config) {
1900
+ const response = await this.fetchList(params, config);
1529
1901
  return {
1530
- nutrition: result.items,
1531
- nextCursor: result.nextCursor,
1532
- hasMore: result.hasMore,
1902
+ biometrics: response.items,
1903
+ nextCursor: response.nextCursor,
1904
+ hasMore: response.hasMore,
1533
1905
  source: this.currentSource()
1534
1906
  };
1535
1907
  }
1536
- async getConnectionStatus(params) {
1537
- const payload = await this.fetchRecord("connection/status", params);
1538
- const status = readString2(payload, "status") ?? "healthy";
1908
+ async fetchNutrition(params, config) {
1909
+ const response = await this.fetchList(params, config);
1539
1910
  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)
1911
+ nutrition: response.items,
1912
+ nextCursor: response.nextCursor,
1913
+ hasMore: response.hasMore,
1914
+ source: this.currentSource()
1548
1915
  };
1549
1916
  }
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);
1917
+ async fetchConnectionStatus(params, config) {
1918
+ const payload = await this.fetchPayload(config, params);
1919
+ return toHealthConnectionStatus(payload, params, this.currentSource());
1564
1920
  }
1565
- async parseWebhook(request) {
1566
- const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
1567
- const body = asRecord(payload);
1921
+ currentSource() {
1568
1922
  return {
1569
1923
  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
1924
+ transport: this.transport,
1925
+ route: this.route,
1926
+ aggregatorKey: this.aggregatorKey
1576
1927
  };
1577
1928
  }
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;
1929
+ providerSlug() {
1930
+ return this.providerKey.replace("health.", "").replace(/-/g, "_");
1931
+ }
1932
+ unsupported(capability) {
1933
+ return new HealthProviderCapabilityError(`${this.providerKey} does not support ${capability}`);
1584
1934
  }
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) ?? [];
1935
+ async syncFromList(executor) {
1936
+ const result = await executor();
1937
+ const records = countResultRecords(result);
1588
1938
  return {
1589
- items,
1590
- nextCursor: readString2(payload, "nextCursor") ?? readString2(payload, "cursor"),
1591
- hasMore: readBoolean2(payload, "hasMore")
1939
+ synced: records,
1940
+ failed: 0,
1941
+ nextCursor: undefined,
1942
+ source: result.source
1592
1943
  };
1593
1944
  }
1594
- async sync(resource, params) {
1595
- const payload = await this.fetchRecord(`sync/${resource}`, params, "POST");
1945
+ async fetchList(params, config) {
1946
+ const payload = await this.fetchPayload(config, params);
1947
+ const items = extractList(payload, config.listKeys).map((item) => config.mapItem(item, params)).filter((item) => Boolean(item));
1948
+ const pagination = extractPagination(payload);
1596
1949
  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()
1950
+ items,
1951
+ nextCursor: pagination.nextCursor,
1952
+ hasMore: pagination.hasMore
1602
1953
  };
1603
1954
  }
1604
- async fetchRecord(resource, params, method = "GET") {
1605
- if (this.transport.endsWith("mcp")) {
1606
- return this.callMcpTool(resource, params);
1955
+ async fetchPayload(config, params) {
1956
+ const method = config.method ?? "GET";
1957
+ const query = config.buildQuery?.(params);
1958
+ const body = config.buildBody?.(params);
1959
+ if (this.isMcpTransport()) {
1960
+ return this.callMcpTool(config.mcpTool, {
1961
+ ...query ?? {},
1962
+ ...body ?? {}
1963
+ });
1964
+ }
1965
+ if (!config.apiPath || !this.apiBaseUrl) {
1966
+ throw new Error(`${this.providerKey} transport is missing an API path.`);
1967
+ }
1968
+ if (method === "POST") {
1969
+ return this.requestApi(config.apiPath, "POST", undefined, body);
1607
1970
  }
1608
- const url = new URL(`${this.apiBaseUrl.replace(/\/$/, "")}/${resource}`);
1609
- if (method === "GET") {
1610
- for (const [key, value] of Object.entries(params)) {
1971
+ return this.requestApi(config.apiPath, "GET", query, undefined);
1972
+ }
1973
+ isMcpTransport() {
1974
+ return this.transport.endsWith("mcp") || this.transport === "unofficial";
1975
+ }
1976
+ async requestApi(path, method, query, body) {
1977
+ const url = new URL(path, ensureTrailingSlash(this.apiBaseUrl ?? ""));
1978
+ if (query) {
1979
+ for (const [key, value] of Object.entries(query)) {
1611
1980
  if (value == null)
1612
1981
  continue;
1613
1982
  if (Array.isArray(value)) {
1614
- value.forEach((item) => {
1615
- url.searchParams.append(key, String(item));
1983
+ value.forEach((entry) => {
1984
+ if (entry != null)
1985
+ url.searchParams.append(key, String(entry));
1616
1986
  });
1617
1987
  continue;
1618
1988
  }
@@ -1621,22 +1991,22 @@ class BaseHealthProvider {
1621
1991
  }
1622
1992
  const response = await this.fetchFn(url, {
1623
1993
  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
1994
+ headers: this.authorizationHeaders(),
1995
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
1629
1996
  });
1630
- if (!response.ok) {
1631
- const errorBody = await safeResponseText(response);
1632
- throw new Error(`${this.providerKey} ${resource} failed (${response.status}): ${errorBody}`);
1997
+ if (response.status === 401 && await this.refreshAccessToken()) {
1998
+ const retryResponse = await this.fetchFn(url, {
1999
+ method,
2000
+ headers: this.authorizationHeaders(),
2001
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
2002
+ });
2003
+ return this.readResponsePayload(retryResponse, path);
1633
2004
  }
1634
- const data = await response.json();
1635
- return asRecord(data) ?? {};
2005
+ return this.readResponsePayload(response, path);
1636
2006
  }
1637
- async callMcpTool(resource, params) {
2007
+ async callMcpTool(toolName, args) {
1638
2008
  if (!this.mcpUrl) {
1639
- return {};
2009
+ throw new Error(`${this.providerKey} MCP URL is not configured.`);
1640
2010
  }
1641
2011
  const response = await this.fetchFn(this.mcpUrl, {
1642
2012
  method: "POST",
@@ -1649,78 +2019,103 @@ class BaseHealthProvider {
1649
2019
  id: ++this.mcpRequestId,
1650
2020
  method: "tools/call",
1651
2021
  params: {
1652
- name: `${this.providerKey.replace("health.", "")}_${resource.replace(/\//g, "_")}`,
1653
- arguments: params
2022
+ name: toolName,
2023
+ arguments: args
1654
2024
  }
1655
2025
  })
1656
2026
  });
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;
2027
+ const payload = await this.readResponsePayload(response, toolName);
2028
+ const rpcEnvelope = asRecord(payload);
2029
+ if (!rpcEnvelope)
2030
+ return payload;
2031
+ const rpcResult = asRecord(rpcEnvelope.result);
2032
+ if (rpcResult) {
2033
+ return rpcResult.structuredContent ?? rpcResult.data ?? rpcResult;
2034
+ }
2035
+ return rpcEnvelope.structuredContent ?? rpcEnvelope.data ?? rpcEnvelope;
1671
2036
  }
1672
- currentSource() {
2037
+ authorizationHeaders() {
2038
+ const token = this.accessToken ?? this.apiKey;
1673
2039
  return {
1674
- providerKey: this.providerKey,
1675
- transport: this.transport,
1676
- route: "primary"
2040
+ "Content-Type": "application/json",
2041
+ ...token ? { Authorization: `Bearer ${token}` } : {}
1677
2042
  };
1678
2043
  }
1679
- }
1680
- function safeJsonParse(raw) {
1681
- try {
1682
- return JSON.parse(raw);
1683
- } catch {
1684
- return { rawBody: raw };
2044
+ async refreshAccessToken() {
2045
+ if (!this.oauth.tokenUrl || !this.refreshToken) {
2046
+ return false;
2047
+ }
2048
+ const tokenUrl = new URL(this.oauth.tokenUrl);
2049
+ const body = new URLSearchParams({
2050
+ grant_type: "refresh_token",
2051
+ refresh_token: this.refreshToken,
2052
+ ...this.oauth.clientId ? { client_id: this.oauth.clientId } : {},
2053
+ ...this.oauth.clientSecret ? { client_secret: this.oauth.clientSecret } : {}
2054
+ });
2055
+ const response = await this.fetchFn(tokenUrl, {
2056
+ method: "POST",
2057
+ headers: {
2058
+ "Content-Type": "application/x-www-form-urlencoded"
2059
+ },
2060
+ body: body.toString()
2061
+ });
2062
+ if (!response.ok) {
2063
+ return false;
2064
+ }
2065
+ const payload = await response.json();
2066
+ this.accessToken = payload.access_token;
2067
+ this.refreshToken = payload.refresh_token ?? this.refreshToken;
2068
+ if (typeof payload.expires_in === "number") {
2069
+ this.oauth.tokenExpiresAt = new Date(Date.now() + payload.expires_in * 1000).toISOString();
2070
+ }
2071
+ return Boolean(this.accessToken);
2072
+ }
2073
+ async readResponsePayload(response, context) {
2074
+ if (!response.ok) {
2075
+ const message = await safeReadText2(response);
2076
+ throw new Error(`${this.providerKey} request ${context} failed (${response.status}): ${message}`);
2077
+ }
2078
+ if (response.status === 204) {
2079
+ return {};
2080
+ }
2081
+ return response.json();
1685
2082
  }
1686
2083
  }
1687
2084
  function readHeader(headers, key) {
1688
- const match = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase());
1689
- if (!match)
2085
+ const target = key.toLowerCase();
2086
+ const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
2087
+ if (!entry)
1690
2088
  return;
1691
- const value = match[1];
2089
+ const value = entry[1];
1692
2090
  return Array.isArray(value) ? value[0] : value;
1693
2091
  }
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;
2092
+ function countResultRecords(result) {
2093
+ const listKeys = [
2094
+ "activities",
2095
+ "workouts",
2096
+ "sleep",
2097
+ "biometrics",
2098
+ "nutrition"
2099
+ ];
2100
+ for (const key of listKeys) {
2101
+ const value = result[key];
2102
+ if (Array.isArray(value)) {
2103
+ return value.length;
2104
+ }
1705
2105
  }
1706
- return value;
1707
- }
1708
- function asArray2(value) {
1709
- return Array.isArray(value) ? value : undefined;
2106
+ return 0;
1710
2107
  }
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;
2108
+ function ensureTrailingSlash(value) {
2109
+ return value.endsWith("/") ? value : `${value}/`;
1718
2110
  }
1719
- function readNumber(record, key) {
1720
- const value = record?.[key];
1721
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2111
+ function safeJsonParse(raw) {
2112
+ try {
2113
+ return JSON.parse(raw);
2114
+ } catch {
2115
+ return { rawBody: raw };
2116
+ }
1722
2117
  }
1723
- async function safeResponseText(response) {
2118
+ async function safeReadText2(response) {
1724
2119
  try {
1725
2120
  return await response.text();
1726
2121
  } catch {
@@ -1728,133 +2123,530 @@ async function safeResponseText(response) {
1728
2123
  }
1729
2124
  }
1730
2125
 
1731
- // src/impls/health/providers.ts
1732
- function createProviderOptions(options, fallbackTransport) {
2126
+ // src/impls/health/official-health-providers.ts
2127
+ function buildSharedQuery(params) {
1733
2128
  return {
1734
- ...options,
1735
- transport: options.transport ?? fallbackTransport
2129
+ tenantId: params.tenantId,
2130
+ connectionId: params.connectionId,
2131
+ userId: params.userId,
2132
+ from: params.from,
2133
+ to: params.to,
2134
+ cursor: params.cursor,
2135
+ pageSize: params.pageSize
2136
+ };
2137
+ }
2138
+ function withMetricTypes(params) {
2139
+ return {
2140
+ ...buildSharedQuery(params),
2141
+ metricTypes: params.metricTypes
1736
2142
  };
1737
2143
  }
1738
2144
 
1739
2145
  class OpenWearablesHealthProvider extends BaseHealthProvider {
2146
+ upstreamProvider;
1740
2147
  constructor(options) {
1741
2148
  super({
1742
- providerKey: "health.openwearables",
1743
- ...createProviderOptions(options, "aggregator-api")
2149
+ providerKey: options.providerKey ?? "health.openwearables",
2150
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.openwearables.io",
2151
+ webhookSignatureHeader: "x-openwearables-signature",
2152
+ ...options
1744
2153
  });
2154
+ this.upstreamProvider = options.upstreamProvider;
1745
2155
  }
1746
- }
1747
-
1748
- class WhoopHealthProvider extends BaseHealthProvider {
1749
- constructor(options) {
1750
- super({
1751
- providerKey: "health.whoop",
1752
- ...createProviderOptions(options, "official-api")
2156
+ async listActivities(params) {
2157
+ return this.fetchActivities(params, {
2158
+ apiPath: "/v1/activities",
2159
+ mcpTool: "openwearables_list_activities",
2160
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
2161
+ mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
1753
2162
  });
1754
2163
  }
1755
- }
1756
-
1757
- class AppleHealthBridgeProvider extends BaseHealthProvider {
1758
- constructor(options) {
1759
- super({
1760
- providerKey: "health.apple-health",
1761
- ...createProviderOptions(options, "aggregator-api")
2164
+ async listWorkouts(params) {
2165
+ return this.fetchWorkouts(params, {
2166
+ apiPath: "/v1/workouts",
2167
+ mcpTool: "openwearables_list_workouts",
2168
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
2169
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
1762
2170
  });
1763
2171
  }
1764
- }
1765
-
1766
- class OuraHealthProvider extends BaseHealthProvider {
1767
- constructor(options) {
1768
- super({
1769
- providerKey: "health.oura",
1770
- ...createProviderOptions(options, "official-api")
2172
+ async listSleep(params) {
2173
+ return this.fetchSleep(params, {
2174
+ apiPath: "/v1/sleep",
2175
+ mcpTool: "openwearables_list_sleep",
2176
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
2177
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
1771
2178
  });
1772
2179
  }
1773
- }
1774
-
1775
- class StravaHealthProvider extends BaseHealthProvider {
1776
- constructor(options) {
1777
- super({
1778
- providerKey: "health.strava",
1779
- ...createProviderOptions(options, "official-api")
2180
+ async listBiometrics(params) {
2181
+ return this.fetchBiometrics(params, {
2182
+ apiPath: "/v1/biometrics",
2183
+ mcpTool: "openwearables_list_biometrics",
2184
+ buildQuery: (input) => this.withUpstreamProvider(withMetricTypes(input)),
2185
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input))
1780
2186
  });
1781
2187
  }
2188
+ async listNutrition(params) {
2189
+ return this.fetchNutrition(params, {
2190
+ apiPath: "/v1/nutrition",
2191
+ mcpTool: "openwearables_list_nutrition",
2192
+ buildQuery: (input) => this.withUpstreamProvider(buildSharedQuery(input)),
2193
+ mapItem: (item, input) => toHealthNutrition(item, this.context(input))
2194
+ });
2195
+ }
2196
+ async getConnectionStatus(params) {
2197
+ return this.fetchConnectionStatus(params, {
2198
+ apiPath: `/v1/connections/${encodeURIComponent(params.connectionId)}/status`,
2199
+ mcpTool: "openwearables_connection_status"
2200
+ });
2201
+ }
2202
+ withUpstreamProvider(query) {
2203
+ return {
2204
+ ...query,
2205
+ ...this.upstreamProvider ? { upstreamProvider: this.upstreamProvider } : {}
2206
+ };
2207
+ }
2208
+ context(params) {
2209
+ return {
2210
+ tenantId: params.tenantId,
2211
+ connectionId: params.connectionId,
2212
+ providerKey: this.providerKey
2213
+ };
2214
+ }
1782
2215
  }
1783
2216
 
1784
- class GarminHealthProvider extends BaseHealthProvider {
2217
+ class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
1785
2218
  constructor(options) {
1786
2219
  super({
1787
- providerKey: "health.garmin",
1788
- ...createProviderOptions(options, "official-api")
2220
+ ...options,
2221
+ providerKey: "health.apple-health",
2222
+ upstreamProvider: "apple-health"
2223
+ });
2224
+ }
2225
+ }
2226
+
2227
+ class WhoopHealthProvider extends BaseHealthProvider {
2228
+ constructor(options) {
2229
+ super({
2230
+ providerKey: "health.whoop",
2231
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.prod.whoop.com",
2232
+ webhookSignatureHeader: "x-whoop-signature",
2233
+ oauth: {
2234
+ tokenUrl: options.oauth?.tokenUrl ?? "https://api.prod.whoop.com/oauth/oauth2/token",
2235
+ ...options.oauth
2236
+ },
2237
+ ...options
2238
+ });
2239
+ }
2240
+ async listActivities(params) {
2241
+ return this.fetchActivities(params, {
2242
+ apiPath: "/v2/activity/workout",
2243
+ mcpTool: "whoop_list_activities",
2244
+ buildQuery: buildSharedQuery,
2245
+ mapItem: (item, input) => toHealthActivity(item, this.context(input), "workout")
2246
+ });
2247
+ }
2248
+ async listWorkouts(params) {
2249
+ return this.fetchWorkouts(params, {
2250
+ apiPath: "/v2/activity/workout",
2251
+ mcpTool: "whoop_list_workouts",
2252
+ buildQuery: buildSharedQuery,
2253
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2254
+ });
2255
+ }
2256
+ async listSleep(params) {
2257
+ return this.fetchSleep(params, {
2258
+ apiPath: "/v2/activity/sleep",
2259
+ mcpTool: "whoop_list_sleep",
2260
+ buildQuery: buildSharedQuery,
2261
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
2262
+ });
2263
+ }
2264
+ async listBiometrics(params) {
2265
+ return this.fetchBiometrics(params, {
2266
+ apiPath: "/v2/recovery",
2267
+ mcpTool: "whoop_list_biometrics",
2268
+ buildQuery: withMetricTypes,
2269
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input), "recovery_score")
2270
+ });
2271
+ }
2272
+ async listNutrition(_params) {
2273
+ throw this.unsupported("nutrition");
2274
+ }
2275
+ async getConnectionStatus(params) {
2276
+ return this.fetchConnectionStatus(params, {
2277
+ apiPath: "/v2/user/profile/basic",
2278
+ mcpTool: "whoop_connection_status"
1789
2279
  });
1790
2280
  }
2281
+ context(params) {
2282
+ return {
2283
+ tenantId: params.tenantId,
2284
+ connectionId: params.connectionId,
2285
+ providerKey: this.providerKey
2286
+ };
2287
+ }
2288
+ }
2289
+
2290
+ class OuraHealthProvider extends BaseHealthProvider {
2291
+ constructor(options) {
2292
+ super({
2293
+ providerKey: "health.oura",
2294
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.ouraring.com",
2295
+ webhookSignatureHeader: "x-oura-signature",
2296
+ oauth: {
2297
+ tokenUrl: options.oauth?.tokenUrl ?? "https://api.ouraring.com/oauth/token",
2298
+ ...options.oauth
2299
+ },
2300
+ ...options
2301
+ });
2302
+ }
2303
+ async listActivities(params) {
2304
+ return this.fetchActivities(params, {
2305
+ apiPath: "/v2/usercollection/daily_activity",
2306
+ mcpTool: "oura_list_activities",
2307
+ buildQuery: buildSharedQuery,
2308
+ mapItem: (item, input) => toHealthActivity(item, this.context(input))
2309
+ });
2310
+ }
2311
+ async listWorkouts(params) {
2312
+ return this.fetchWorkouts(params, {
2313
+ apiPath: "/v2/usercollection/workout",
2314
+ mcpTool: "oura_list_workouts",
2315
+ buildQuery: buildSharedQuery,
2316
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2317
+ });
2318
+ }
2319
+ async listSleep(params) {
2320
+ return this.fetchSleep(params, {
2321
+ apiPath: "/v2/usercollection/sleep",
2322
+ mcpTool: "oura_list_sleep",
2323
+ buildQuery: buildSharedQuery,
2324
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
2325
+ });
2326
+ }
2327
+ async listBiometrics(params) {
2328
+ return this.fetchBiometrics(params, {
2329
+ apiPath: "/v2/usercollection/daily_readiness",
2330
+ mcpTool: "oura_list_biometrics",
2331
+ buildQuery: withMetricTypes,
2332
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input), "readiness_score")
2333
+ });
2334
+ }
2335
+ async listNutrition(_params) {
2336
+ throw this.unsupported("nutrition");
2337
+ }
2338
+ async getConnectionStatus(params) {
2339
+ return this.fetchConnectionStatus(params, {
2340
+ apiPath: "/v2/usercollection/personal_info",
2341
+ mcpTool: "oura_connection_status"
2342
+ });
2343
+ }
2344
+ context(params) {
2345
+ return {
2346
+ tenantId: params.tenantId,
2347
+ connectionId: params.connectionId,
2348
+ providerKey: this.providerKey
2349
+ };
2350
+ }
2351
+ }
2352
+
2353
+ class StravaHealthProvider extends BaseHealthProvider {
2354
+ constructor(options) {
2355
+ super({
2356
+ providerKey: "health.strava",
2357
+ apiBaseUrl: options.apiBaseUrl ?? "https://www.strava.com",
2358
+ webhookSignatureHeader: "x-strava-signature",
2359
+ oauth: {
2360
+ tokenUrl: options.oauth?.tokenUrl ?? "https://www.strava.com/oauth/token",
2361
+ ...options.oauth
2362
+ },
2363
+ ...options
2364
+ });
2365
+ }
2366
+ async listActivities(params) {
2367
+ return this.fetchActivities(params, {
2368
+ apiPath: "/api/v3/athlete/activities",
2369
+ mcpTool: "strava_list_activities",
2370
+ buildQuery: buildSharedQuery,
2371
+ mapItem: (item, input) => toHealthActivity(item, this.context(input))
2372
+ });
2373
+ }
2374
+ async listWorkouts(params) {
2375
+ return this.fetchWorkouts(params, {
2376
+ apiPath: "/api/v3/athlete/activities",
2377
+ mcpTool: "strava_list_workouts",
2378
+ buildQuery: buildSharedQuery,
2379
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2380
+ });
2381
+ }
2382
+ async listSleep(_params) {
2383
+ throw this.unsupported("sleep");
2384
+ }
2385
+ async listBiometrics(_params) {
2386
+ throw this.unsupported("biometrics");
2387
+ }
2388
+ async listNutrition(_params) {
2389
+ throw this.unsupported("nutrition");
2390
+ }
2391
+ async getConnectionStatus(params) {
2392
+ return this.fetchConnectionStatus(params, {
2393
+ apiPath: "/api/v3/athlete",
2394
+ mcpTool: "strava_connection_status"
2395
+ });
2396
+ }
2397
+ context(params) {
2398
+ return {
2399
+ tenantId: params.tenantId,
2400
+ connectionId: params.connectionId,
2401
+ providerKey: this.providerKey
2402
+ };
2403
+ }
1791
2404
  }
1792
2405
 
1793
2406
  class FitbitHealthProvider extends BaseHealthProvider {
1794
2407
  constructor(options) {
1795
2408
  super({
1796
2409
  providerKey: "health.fitbit",
1797
- ...createProviderOptions(options, "official-api")
2410
+ apiBaseUrl: options.apiBaseUrl ?? "https://api.fitbit.com",
2411
+ webhookSignatureHeader: "x-fitbit-signature",
2412
+ oauth: {
2413
+ tokenUrl: options.oauth?.tokenUrl ?? "https://api.fitbit.com/oauth2/token",
2414
+ ...options.oauth
2415
+ },
2416
+ ...options
2417
+ });
2418
+ }
2419
+ async listActivities(params) {
2420
+ return this.fetchActivities(params, {
2421
+ apiPath: "/1/user/-/activities/list.json",
2422
+ mcpTool: "fitbit_list_activities",
2423
+ buildQuery: buildSharedQuery,
2424
+ mapItem: (item, input) => toHealthActivity(item, this.context(input))
2425
+ });
2426
+ }
2427
+ async listWorkouts(params) {
2428
+ return this.fetchWorkouts(params, {
2429
+ apiPath: "/1/user/-/activities/list.json",
2430
+ mcpTool: "fitbit_list_workouts",
2431
+ buildQuery: buildSharedQuery,
2432
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2433
+ });
2434
+ }
2435
+ async listSleep(params) {
2436
+ return this.fetchSleep(params, {
2437
+ apiPath: "/1.2/user/-/sleep/list.json",
2438
+ mcpTool: "fitbit_list_sleep",
2439
+ buildQuery: buildSharedQuery,
2440
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
2441
+ });
2442
+ }
2443
+ async listBiometrics(params) {
2444
+ return this.fetchBiometrics(params, {
2445
+ apiPath: "/1/user/-/body/log/weight/date/today/1m.json",
2446
+ mcpTool: "fitbit_list_biometrics",
2447
+ buildQuery: withMetricTypes,
2448
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input), "weight")
1798
2449
  });
1799
2450
  }
2451
+ async listNutrition(params) {
2452
+ return this.fetchNutrition(params, {
2453
+ apiPath: "/1/user/-/foods/log/date/today.json",
2454
+ mcpTool: "fitbit_list_nutrition",
2455
+ buildQuery: buildSharedQuery,
2456
+ mapItem: (item, input) => toHealthNutrition(item, this.context(input))
2457
+ });
2458
+ }
2459
+ async getConnectionStatus(params) {
2460
+ return this.fetchConnectionStatus(params, {
2461
+ apiPath: "/1/user/-/profile.json",
2462
+ mcpTool: "fitbit_connection_status"
2463
+ });
2464
+ }
2465
+ context(params) {
2466
+ return {
2467
+ tenantId: params.tenantId,
2468
+ connectionId: params.connectionId,
2469
+ providerKey: this.providerKey
2470
+ };
2471
+ }
1800
2472
  }
1801
2473
 
1802
- class MyFitnessPalHealthProvider extends BaseHealthProvider {
2474
+ // src/impls/health/hybrid-health-providers.ts
2475
+ var LIMITED_PROVIDER_SLUG = {
2476
+ "health.garmin": "garmin",
2477
+ "health.myfitnesspal": "myfitnesspal",
2478
+ "health.eightsleep": "eightsleep",
2479
+ "health.peloton": "peloton"
2480
+ };
2481
+ function buildSharedQuery2(params) {
2482
+ return {
2483
+ tenantId: params.tenantId,
2484
+ connectionId: params.connectionId,
2485
+ userId: params.userId,
2486
+ from: params.from,
2487
+ to: params.to,
2488
+ cursor: params.cursor,
2489
+ pageSize: params.pageSize
2490
+ };
2491
+ }
2492
+
2493
+ class GarminHealthProvider extends OpenWearablesHealthProvider {
1803
2494
  constructor(options) {
1804
2495
  super({
2496
+ ...options,
2497
+ providerKey: "health.garmin",
2498
+ upstreamProvider: "garmin"
2499
+ });
2500
+ }
2501
+ }
2502
+
2503
+ class MyFitnessPalHealthProvider extends OpenWearablesHealthProvider {
2504
+ constructor(options) {
2505
+ super({
2506
+ ...options,
1805
2507
  providerKey: "health.myfitnesspal",
1806
- ...createProviderOptions(options, "official-api")
2508
+ upstreamProvider: "myfitnesspal"
1807
2509
  });
1808
2510
  }
1809
2511
  }
1810
2512
 
1811
- class EightSleepHealthProvider extends BaseHealthProvider {
2513
+ class EightSleepHealthProvider extends OpenWearablesHealthProvider {
1812
2514
  constructor(options) {
1813
2515
  super({
2516
+ ...options,
1814
2517
  providerKey: "health.eightsleep",
1815
- ...createProviderOptions(options, "official-api")
2518
+ upstreamProvider: "eightsleep"
1816
2519
  });
1817
2520
  }
1818
2521
  }
1819
2522
 
1820
- class PelotonHealthProvider extends BaseHealthProvider {
2523
+ class PelotonHealthProvider extends OpenWearablesHealthProvider {
1821
2524
  constructor(options) {
1822
2525
  super({
2526
+ ...options,
1823
2527
  providerKey: "health.peloton",
1824
- ...createProviderOptions(options, "official-api")
2528
+ upstreamProvider: "peloton"
1825
2529
  });
1826
2530
  }
1827
2531
  }
1828
2532
 
1829
2533
  class UnofficialHealthAutomationProvider extends BaseHealthProvider {
2534
+ providerSlugValue;
1830
2535
  constructor(options) {
1831
2536
  super({
1832
- ...createProviderOptions(options, "unofficial"),
1833
- providerKey: options.providerKey
2537
+ ...options,
2538
+ providerKey: options.providerKey,
2539
+ webhookSignatureHeader: "x-unofficial-signature"
1834
2540
  });
2541
+ this.providerSlugValue = LIMITED_PROVIDER_SLUG[options.providerKey];
2542
+ }
2543
+ async listActivities(params) {
2544
+ return this.fetchActivities(params, {
2545
+ mcpTool: `${this.providerSlugValue}_list_activities`,
2546
+ buildQuery: buildSharedQuery2,
2547
+ mapItem: (item, input) => toHealthActivity(item, this.context(input), "activity")
2548
+ });
2549
+ }
2550
+ async listWorkouts(params) {
2551
+ return this.fetchWorkouts(params, {
2552
+ mcpTool: `${this.providerSlugValue}_list_workouts`,
2553
+ buildQuery: buildSharedQuery2,
2554
+ mapItem: (item, input) => toHealthWorkout(item, this.context(input))
2555
+ });
2556
+ }
2557
+ async listSleep(params) {
2558
+ return this.fetchSleep(params, {
2559
+ mcpTool: `${this.providerSlugValue}_list_sleep`,
2560
+ buildQuery: buildSharedQuery2,
2561
+ mapItem: (item, input) => toHealthSleep(item, this.context(input))
2562
+ });
2563
+ }
2564
+ async listBiometrics(params) {
2565
+ return this.fetchBiometrics(params, {
2566
+ mcpTool: `${this.providerSlugValue}_list_biometrics`,
2567
+ buildQuery: (input) => ({
2568
+ ...buildSharedQuery2(input),
2569
+ metricTypes: input.metricTypes
2570
+ }),
2571
+ mapItem: (item, input) => toHealthBiometric(item, this.context(input))
2572
+ });
2573
+ }
2574
+ async listNutrition(params) {
2575
+ return this.fetchNutrition(params, {
2576
+ mcpTool: `${this.providerSlugValue}_list_nutrition`,
2577
+ buildQuery: buildSharedQuery2,
2578
+ mapItem: (item, input) => toHealthNutrition(item, this.context(input))
2579
+ });
2580
+ }
2581
+ async getConnectionStatus(params) {
2582
+ return this.fetchConnectionStatus(params, {
2583
+ mcpTool: `${this.providerSlugValue}_connection_status`
2584
+ });
2585
+ }
2586
+ context(params) {
2587
+ return {
2588
+ tenantId: params.tenantId,
2589
+ connectionId: params.connectionId,
2590
+ providerKey: this.providerKey
2591
+ };
1835
2592
  }
1836
2593
  }
1837
-
1838
2594
  // src/impls/health-provider-factory.ts
1839
2595
  import {
1840
2596
  isUnofficialHealthProviderAllowed,
1841
2597
  resolveHealthStrategyOrder
1842
2598
  } from "@contractspec/integration.runtime/runtime";
2599
+ var OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER = {
2600
+ "health.openwearables": false,
2601
+ "health.whoop": true,
2602
+ "health.apple-health": false,
2603
+ "health.oura": true,
2604
+ "health.strava": true,
2605
+ "health.garmin": false,
2606
+ "health.fitbit": true,
2607
+ "health.myfitnesspal": false,
2608
+ "health.eightsleep": false,
2609
+ "health.peloton": false
2610
+ };
2611
+ var UNOFFICIAL_SUPPORTED_BY_PROVIDER = {
2612
+ "health.openwearables": false,
2613
+ "health.whoop": false,
2614
+ "health.apple-health": false,
2615
+ "health.oura": false,
2616
+ "health.strava": false,
2617
+ "health.garmin": true,
2618
+ "health.fitbit": false,
2619
+ "health.myfitnesspal": true,
2620
+ "health.eightsleep": true,
2621
+ "health.peloton": true
2622
+ };
1843
2623
  function createHealthProviderFromContext(context, secrets) {
1844
2624
  const providerKey = context.spec.meta.key;
1845
2625
  const config = toFactoryConfig(context.config);
1846
2626
  const strategyOrder = buildStrategyOrder(config);
1847
- const errors = [];
1848
- for (const strategy of strategyOrder) {
1849
- const provider = createHealthProviderForStrategy(providerKey, strategy, config, secrets);
2627
+ const attemptLogs = [];
2628
+ for (let index = 0;index < strategyOrder.length; index += 1) {
2629
+ const strategy = strategyOrder[index];
2630
+ if (!strategy)
2631
+ continue;
2632
+ const route = index === 0 ? "primary" : "fallback";
2633
+ if (!supportsStrategy(providerKey, strategy)) {
2634
+ attemptLogs.push(`${strategy}: unsupported by ${providerKey}`);
2635
+ continue;
2636
+ }
2637
+ if (!hasCredentialsForStrategy(strategy, config, secrets)) {
2638
+ attemptLogs.push(`${strategy}: missing credentials`);
2639
+ continue;
2640
+ }
2641
+ const provider = createHealthProviderForStrategy(providerKey, strategy, route, config, secrets);
1850
2642
  if (provider) {
1851
2643
  return provider;
1852
2644
  }
1853
- errors.push(`${strategy}: not available`);
2645
+ attemptLogs.push(`${strategy}: not available`);
1854
2646
  }
1855
- throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${errors.join(", ")}.`);
2647
+ throw new Error(`Unable to resolve health provider for ${providerKey}. Strategies attempted: ${attemptLogs.join(", ")}.`);
1856
2648
  }
1857
- function createHealthProviderForStrategy(providerKey, strategy, config, secrets) {
2649
+ function createHealthProviderForStrategy(providerKey, strategy, route, config, secrets) {
1858
2650
  const options = {
1859
2651
  transport: strategy,
1860
2652
  apiBaseUrl: config.apiBaseUrl,
@@ -1862,10 +2654,21 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
1862
2654
  apiKey: getSecretString(secrets, "apiKey"),
1863
2655
  accessToken: getSecretString(secrets, "accessToken"),
1864
2656
  mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
1865
- webhookSecret: getSecretString(secrets, "webhookSecret")
2657
+ webhookSecret: getSecretString(secrets, "webhookSecret"),
2658
+ route,
2659
+ oauth: {
2660
+ tokenUrl: config.oauthTokenUrl,
2661
+ refreshToken: getSecretString(secrets, "refreshToken"),
2662
+ clientId: getSecretString(secrets, "clientId"),
2663
+ clientSecret: getSecretString(secrets, "clientSecret"),
2664
+ tokenExpiresAt: getSecretString(secrets, "tokenExpiresAt")
2665
+ }
1866
2666
  };
1867
2667
  if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
1868
- return new OpenWearablesHealthProvider(options);
2668
+ return createAggregatorProvider(providerKey, {
2669
+ ...options,
2670
+ aggregatorKey: "health.openwearables"
2671
+ });
1869
2672
  }
1870
2673
  if (strategy === "unofficial") {
1871
2674
  if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
@@ -1887,6 +2690,31 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
1887
2690
  }
1888
2691
  return createOfficialProvider(providerKey, options);
1889
2692
  }
2693
+ function createAggregatorProvider(providerKey, options) {
2694
+ if (providerKey === "health.apple-health") {
2695
+ return new AppleHealthBridgeProvider(options);
2696
+ }
2697
+ if (providerKey === "health.garmin") {
2698
+ return new GarminHealthProvider(options);
2699
+ }
2700
+ if (providerKey === "health.myfitnesspal") {
2701
+ return new MyFitnessPalHealthProvider(options);
2702
+ }
2703
+ if (providerKey === "health.eightsleep") {
2704
+ return new EightSleepHealthProvider(options);
2705
+ }
2706
+ if (providerKey === "health.peloton") {
2707
+ return new PelotonHealthProvider(options);
2708
+ }
2709
+ if (providerKey === "health.openwearables") {
2710
+ return new OpenWearablesHealthProvider(options);
2711
+ }
2712
+ return new OpenWearablesHealthProvider({
2713
+ ...options,
2714
+ providerKey,
2715
+ upstreamProvider: providerKey.replace("health.", "")
2716
+ });
2717
+ }
1890
2718
  function createOfficialProvider(providerKey, options) {
1891
2719
  switch (providerKey) {
1892
2720
  case "health.openwearables":
@@ -1904,11 +2732,20 @@ function createOfficialProvider(providerKey, options) {
1904
2732
  case "health.fitbit":
1905
2733
  return new FitbitHealthProvider(options);
1906
2734
  case "health.myfitnesspal":
1907
- return new MyFitnessPalHealthProvider(options);
2735
+ return new MyFitnessPalHealthProvider({
2736
+ ...options,
2737
+ transport: "aggregator-api"
2738
+ });
1908
2739
  case "health.eightsleep":
1909
- return new EightSleepHealthProvider(options);
2740
+ return new EightSleepHealthProvider({
2741
+ ...options,
2742
+ transport: "aggregator-api"
2743
+ });
1910
2744
  case "health.peloton":
1911
- return new PelotonHealthProvider(options);
2745
+ return new PelotonHealthProvider({
2746
+ ...options,
2747
+ transport: "aggregator-api"
2748
+ });
1912
2749
  default:
1913
2750
  throw new Error(`Unsupported health provider key: ${providerKey}`);
1914
2751
  }
@@ -1921,6 +2758,7 @@ function toFactoryConfig(config) {
1921
2758
  return {
1922
2759
  apiBaseUrl: asString(record.apiBaseUrl),
1923
2760
  mcpUrl: asString(record.mcpUrl),
2761
+ oauthTokenUrl: asString(record.oauthTokenUrl),
1924
2762
  defaultTransport: normalizeTransport(record.defaultTransport),
1925
2763
  strategyOrder: normalizeTransportArray(record.strategyOrder),
1926
2764
  allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
@@ -1949,6 +2787,27 @@ function normalizeTransportArray(value) {
1949
2787
  const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
1950
2788
  return transports.length > 0 ? transports : undefined;
1951
2789
  }
2790
+ function supportsStrategy(providerKey, strategy) {
2791
+ if (strategy === "official-api" || strategy === "official-mcp") {
2792
+ return OFFICIAL_TRANSPORT_SUPPORTED_BY_PROVIDER[providerKey];
2793
+ }
2794
+ if (strategy === "unofficial") {
2795
+ return UNOFFICIAL_SUPPORTED_BY_PROVIDER[providerKey];
2796
+ }
2797
+ return true;
2798
+ }
2799
+ function hasCredentialsForStrategy(strategy, config, secrets) {
2800
+ const hasApiCredential = Boolean(getSecretString(secrets, "accessToken")) || Boolean(getSecretString(secrets, "apiKey"));
2801
+ const hasMcpCredential = Boolean(getSecretString(secrets, "mcpAccessToken")) || hasApiCredential;
2802
+ if (strategy === "official-api" || strategy === "aggregator-api") {
2803
+ return hasApiCredential;
2804
+ }
2805
+ if (strategy === "official-mcp" || strategy === "aggregator-mcp") {
2806
+ return Boolean(config.mcpUrl) && hasMcpCredential;
2807
+ }
2808
+ const hasAutomationCredential = hasMcpCredential || Boolean(getSecretString(secrets, "username")) && Boolean(getSecretString(secrets, "password"));
2809
+ return Boolean(config.mcpUrl) && hasAutomationCredential;
2810
+ }
1952
2811
  function getSecretString(secrets, key) {
1953
2812
  const value = secrets[key];
1954
2813
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
@@ -2224,45 +3083,474 @@ function mapFinishReason(reason) {
2224
3083
  }
2225
3084
  }
2226
3085
 
2227
- // src/impls/mistral-embedding.ts
2228
- import { Mistral as Mistral2 } from "@mistralai/mistralai";
3086
+ // src/impls/mistral-embedding.ts
3087
+ import { Mistral as Mistral2 } from "@mistralai/mistralai";
3088
+
3089
+ class MistralEmbeddingProvider {
3090
+ client;
3091
+ defaultModel;
3092
+ constructor(options) {
3093
+ if (!options.apiKey) {
3094
+ throw new Error("MistralEmbeddingProvider requires an apiKey");
3095
+ }
3096
+ this.client = options.client ?? new Mistral2({
3097
+ apiKey: options.apiKey,
3098
+ serverURL: options.serverURL
3099
+ });
3100
+ this.defaultModel = options.defaultModel ?? "mistral-embed";
3101
+ }
3102
+ async embedDocuments(documents, options) {
3103
+ if (documents.length === 0)
3104
+ return [];
3105
+ const model = options?.model ?? this.defaultModel;
3106
+ const response = await this.client.embeddings.create({
3107
+ model,
3108
+ inputs: documents.map((doc) => doc.text)
3109
+ });
3110
+ return response.data.map((item, index) => ({
3111
+ id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
3112
+ vector: item.embedding ?? [],
3113
+ dimensions: item.embedding?.length ?? 0,
3114
+ model: response.model,
3115
+ metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
3116
+ }));
3117
+ }
3118
+ async embedQuery(query, options) {
3119
+ const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
3120
+ if (!result) {
3121
+ throw new Error("Failed to compute embedding for query");
3122
+ }
3123
+ return result;
3124
+ }
3125
+ }
3126
+
3127
+ // src/impls/mistral-stt.ts
3128
+ var DEFAULT_BASE_URL4 = "https://api.mistral.ai/v1";
3129
+ var DEFAULT_MODEL = "voxtral-mini-latest";
3130
+ var AUDIO_MIME_BY_FORMAT = {
3131
+ mp3: "audio/mpeg",
3132
+ wav: "audio/wav",
3133
+ ogg: "audio/ogg",
3134
+ pcm: "audio/pcm",
3135
+ opus: "audio/opus"
3136
+ };
3137
+
3138
+ class MistralSttProvider {
3139
+ apiKey;
3140
+ defaultModel;
3141
+ defaultLanguage;
3142
+ baseUrl;
3143
+ fetchImpl;
3144
+ constructor(options) {
3145
+ if (!options.apiKey) {
3146
+ throw new Error("MistralSttProvider requires an apiKey");
3147
+ }
3148
+ this.apiKey = options.apiKey;
3149
+ this.defaultModel = options.defaultModel ?? DEFAULT_MODEL;
3150
+ this.defaultLanguage = options.defaultLanguage;
3151
+ this.baseUrl = normalizeBaseUrl(options.serverURL ?? DEFAULT_BASE_URL4);
3152
+ this.fetchImpl = options.fetchImpl ?? fetch;
3153
+ }
3154
+ async transcribe(input) {
3155
+ const formData = new FormData;
3156
+ const model = input.model ?? this.defaultModel;
3157
+ const mimeType = AUDIO_MIME_BY_FORMAT[input.audio.format] ?? "audio/wav";
3158
+ const fileName = `audio.${input.audio.format}`;
3159
+ const audioBytes = new Uint8Array(input.audio.data);
3160
+ const blob = new Blob([audioBytes], { type: mimeType });
3161
+ formData.append("file", blob, fileName);
3162
+ formData.append("model", model);
3163
+ formData.append("response_format", "verbose_json");
3164
+ const language = input.language ?? this.defaultLanguage;
3165
+ if (language) {
3166
+ formData.append("language", language);
3167
+ }
3168
+ const response = await this.fetchImpl(`${this.baseUrl}/audio/transcriptions`, {
3169
+ method: "POST",
3170
+ headers: {
3171
+ Authorization: `Bearer ${this.apiKey}`
3172
+ },
3173
+ body: formData
3174
+ });
3175
+ if (!response.ok) {
3176
+ const body = await response.text();
3177
+ throw new Error(`Mistral transcription request failed (${response.status}): ${body}`);
3178
+ }
3179
+ const payload = await response.json();
3180
+ return toTranscriptionResult(payload, input);
3181
+ }
3182
+ }
3183
+ function toTranscriptionResult(payload, input) {
3184
+ const record = asRecord2(payload);
3185
+ const text = readString3(record, "text") ?? "";
3186
+ const language = readString3(record, "language") ?? input.language ?? "unknown";
3187
+ const segments = parseSegments(record);
3188
+ if (segments.length === 0 && text.length > 0) {
3189
+ segments.push({
3190
+ text,
3191
+ startMs: 0,
3192
+ endMs: input.audio.durationMs ?? 0
3193
+ });
3194
+ }
3195
+ const durationMs = input.audio.durationMs ?? segments.reduce((max, segment) => Math.max(max, segment.endMs), 0);
3196
+ const topLevelWords = parseWordTimings(record.words);
3197
+ const flattenedWords = segments.flatMap((segment) => segment.wordTimings ?? []);
3198
+ const wordTimings = topLevelWords.length > 0 ? topLevelWords : flattenedWords.length > 0 ? flattenedWords : undefined;
3199
+ const speakers = dedupeSpeakers(segments);
3200
+ return {
3201
+ text,
3202
+ segments,
3203
+ language,
3204
+ durationMs,
3205
+ speakers: speakers.length > 0 ? speakers : undefined,
3206
+ wordTimings
3207
+ };
3208
+ }
3209
+ function parseSegments(record) {
3210
+ if (!Array.isArray(record.segments)) {
3211
+ return [];
3212
+ }
3213
+ const parsed = [];
3214
+ for (const entry of record.segments) {
3215
+ const segmentRecord = asRecord2(entry);
3216
+ const text = readString3(segmentRecord, "text");
3217
+ if (!text) {
3218
+ continue;
3219
+ }
3220
+ const startSeconds = readNumber2(segmentRecord, "start") ?? 0;
3221
+ const endSeconds = readNumber2(segmentRecord, "end") ?? startSeconds;
3222
+ parsed.push({
3223
+ text,
3224
+ startMs: secondsToMs(startSeconds),
3225
+ endMs: secondsToMs(endSeconds),
3226
+ speakerId: readString3(segmentRecord, "speaker") ?? undefined,
3227
+ confidence: readNumber2(segmentRecord, "confidence"),
3228
+ wordTimings: parseWordTimings(segmentRecord.words)
3229
+ });
3230
+ }
3231
+ return parsed;
3232
+ }
3233
+ function parseWordTimings(value) {
3234
+ if (!Array.isArray(value)) {
3235
+ return [];
3236
+ }
3237
+ const words = [];
3238
+ for (const entry of value) {
3239
+ const wordRecord = asRecord2(entry);
3240
+ const word = readString3(wordRecord, "word");
3241
+ const startSeconds = readNumber2(wordRecord, "start");
3242
+ const endSeconds = readNumber2(wordRecord, "end");
3243
+ if (!word || startSeconds == null || endSeconds == null) {
3244
+ continue;
3245
+ }
3246
+ words.push({
3247
+ word,
3248
+ startMs: secondsToMs(startSeconds),
3249
+ endMs: secondsToMs(endSeconds),
3250
+ confidence: readNumber2(wordRecord, "confidence")
3251
+ });
3252
+ }
3253
+ return words;
3254
+ }
3255
+ function dedupeSpeakers(segments) {
3256
+ const seen = new Set;
3257
+ const speakers = [];
3258
+ for (const segment of segments) {
3259
+ if (!segment.speakerId || seen.has(segment.speakerId)) {
3260
+ continue;
3261
+ }
3262
+ seen.add(segment.speakerId);
3263
+ speakers.push({
3264
+ id: segment.speakerId,
3265
+ name: segment.speakerName
3266
+ });
3267
+ }
3268
+ return speakers;
3269
+ }
3270
+ function normalizeBaseUrl(url) {
3271
+ return url.endsWith("/") ? url.slice(0, -1) : url;
3272
+ }
3273
+ function asRecord2(value) {
3274
+ if (value && typeof value === "object") {
3275
+ return value;
3276
+ }
3277
+ return {};
3278
+ }
3279
+ function readString3(record, key) {
3280
+ const value = record[key];
3281
+ return typeof value === "string" ? value : undefined;
3282
+ }
3283
+ function readNumber2(record, key) {
3284
+ const value = record[key];
3285
+ return typeof value === "number" ? value : undefined;
3286
+ }
3287
+ function secondsToMs(value) {
3288
+ return Math.round(value * 1000);
3289
+ }
3290
+
3291
+ // src/impls/mistral-conversational.session.ts
3292
+ class MistralConversationSession {
3293
+ events;
3294
+ queue = new AsyncEventQueue;
3295
+ turns = [];
3296
+ history = [];
3297
+ sessionId = crypto.randomUUID();
3298
+ startedAt = Date.now();
3299
+ sessionConfig;
3300
+ defaultModel;
3301
+ complete;
3302
+ sttProvider;
3303
+ pending = Promise.resolve();
3304
+ closed = false;
3305
+ closedSummary;
3306
+ constructor(options) {
3307
+ this.sessionConfig = options.sessionConfig;
3308
+ this.defaultModel = options.defaultModel;
3309
+ this.complete = options.complete;
3310
+ this.sttProvider = options.sttProvider;
3311
+ this.events = this.queue;
3312
+ this.queue.push({
3313
+ type: "session_started",
3314
+ sessionId: this.sessionId
3315
+ });
3316
+ }
3317
+ sendAudio(chunk) {
3318
+ if (this.closed) {
3319
+ return;
3320
+ }
3321
+ this.pending = this.pending.then(async () => {
3322
+ const transcription = await this.sttProvider.transcribe({
3323
+ audio: {
3324
+ data: chunk,
3325
+ format: this.sessionConfig.inputFormat ?? "pcm",
3326
+ sampleRateHz: 16000
3327
+ },
3328
+ language: this.sessionConfig.language
3329
+ });
3330
+ const transcriptText = transcription.text.trim();
3331
+ if (transcriptText.length > 0) {
3332
+ await this.handleUserText(transcriptText);
3333
+ }
3334
+ }).catch((error) => {
3335
+ this.emitError(error);
3336
+ });
3337
+ }
3338
+ sendText(text) {
3339
+ if (this.closed) {
3340
+ return;
3341
+ }
3342
+ const normalized = text.trim();
3343
+ if (normalized.length === 0) {
3344
+ return;
3345
+ }
3346
+ this.pending = this.pending.then(() => this.handleUserText(normalized)).catch((error) => {
3347
+ this.emitError(error);
3348
+ });
3349
+ }
3350
+ interrupt() {
3351
+ if (this.closed) {
3352
+ return;
3353
+ }
3354
+ this.queue.push({
3355
+ type: "error",
3356
+ error: new Error("Interrupt is not supported for non-streaming sessions.")
3357
+ });
3358
+ }
3359
+ async close() {
3360
+ if (this.closedSummary) {
3361
+ return this.closedSummary;
3362
+ }
3363
+ this.closed = true;
3364
+ await this.pending;
3365
+ const durationMs = Date.now() - this.startedAt;
3366
+ const summary = {
3367
+ sessionId: this.sessionId,
3368
+ durationMs,
3369
+ turns: this.turns.map((turn) => ({
3370
+ role: turn.role === "assistant" ? "agent" : turn.role,
3371
+ text: turn.text,
3372
+ startMs: turn.startMs,
3373
+ endMs: turn.endMs
3374
+ })),
3375
+ transcript: this.turns.map((turn) => `${turn.role}: ${turn.text}`).join(`
3376
+ `)
3377
+ };
3378
+ this.closedSummary = summary;
3379
+ this.queue.push({
3380
+ type: "session_ended",
3381
+ reason: "closed_by_client",
3382
+ durationMs
3383
+ });
3384
+ this.queue.close();
3385
+ return summary;
3386
+ }
3387
+ async handleUserText(text) {
3388
+ if (this.closed) {
3389
+ return;
3390
+ }
3391
+ const userStart = Date.now();
3392
+ this.queue.push({ type: "user_speech_started" });
3393
+ this.queue.push({ type: "user_speech_ended", transcript: text });
3394
+ this.queue.push({
3395
+ type: "transcript",
3396
+ role: "user",
3397
+ text,
3398
+ timestamp: userStart
3399
+ });
3400
+ this.turns.push({
3401
+ role: "user",
3402
+ text,
3403
+ startMs: userStart,
3404
+ endMs: Date.now()
3405
+ });
3406
+ this.history.push({ role: "user", content: text });
3407
+ const assistantStart = Date.now();
3408
+ const assistantText = await this.complete(this.history, {
3409
+ ...this.sessionConfig,
3410
+ llmModel: this.sessionConfig.llmModel ?? this.defaultModel
3411
+ });
3412
+ if (this.closed) {
3413
+ return;
3414
+ }
3415
+ const normalizedAssistantText = assistantText.trim();
3416
+ const finalAssistantText = normalizedAssistantText.length > 0 ? normalizedAssistantText : "I was unable to produce a response.";
3417
+ this.queue.push({
3418
+ type: "agent_speech_started",
3419
+ text: finalAssistantText
3420
+ });
3421
+ this.queue.push({
3422
+ type: "transcript",
3423
+ role: "agent",
3424
+ text: finalAssistantText,
3425
+ timestamp: assistantStart
3426
+ });
3427
+ this.queue.push({ type: "agent_speech_ended" });
3428
+ this.turns.push({
3429
+ role: "assistant",
3430
+ text: finalAssistantText,
3431
+ startMs: assistantStart,
3432
+ endMs: Date.now()
3433
+ });
3434
+ this.history.push({ role: "assistant", content: finalAssistantText });
3435
+ }
3436
+ emitError(error) {
3437
+ if (this.closed) {
3438
+ return;
3439
+ }
3440
+ this.queue.push({ type: "error", error: toError(error) });
3441
+ }
3442
+ }
3443
+ function toError(error) {
3444
+ if (error instanceof Error) {
3445
+ return error;
3446
+ }
3447
+ return new Error(String(error));
3448
+ }
3449
+
3450
+ // src/impls/mistral-conversational.ts
3451
+ var DEFAULT_BASE_URL5 = "https://api.mistral.ai/v1";
3452
+ var DEFAULT_MODEL2 = "mistral-small-latest";
3453
+ var DEFAULT_VOICE = "default";
2229
3454
 
2230
- class MistralEmbeddingProvider {
2231
- client;
3455
+ class MistralConversationalProvider {
3456
+ apiKey;
2232
3457
  defaultModel;
3458
+ defaultVoiceId;
3459
+ baseUrl;
3460
+ fetchImpl;
3461
+ sttProvider;
2233
3462
  constructor(options) {
2234
3463
  if (!options.apiKey) {
2235
- throw new Error("MistralEmbeddingProvider requires an apiKey");
3464
+ throw new Error("MistralConversationalProvider requires an apiKey");
2236
3465
  }
2237
- this.client = options.client ?? new Mistral2({
3466
+ this.apiKey = options.apiKey;
3467
+ this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
3468
+ this.defaultVoiceId = options.defaultVoiceId ?? DEFAULT_VOICE;
3469
+ this.baseUrl = normalizeBaseUrl2(options.serverURL ?? DEFAULT_BASE_URL5);
3470
+ this.fetchImpl = options.fetchImpl ?? fetch;
3471
+ this.sttProvider = options.sttProvider ?? new MistralSttProvider({
2238
3472
  apiKey: options.apiKey,
2239
- serverURL: options.serverURL
3473
+ defaultModel: options.sttOptions?.defaultModel,
3474
+ defaultLanguage: options.sttOptions?.defaultLanguage,
3475
+ serverURL: options.sttOptions?.serverURL ?? options.serverURL,
3476
+ fetchImpl: this.fetchImpl
2240
3477
  });
2241
- this.defaultModel = options.defaultModel ?? "mistral-embed";
2242
3478
  }
2243
- async embedDocuments(documents, options) {
2244
- if (documents.length === 0)
2245
- return [];
2246
- const model = options?.model ?? this.defaultModel;
2247
- const response = await this.client.embeddings.create({
2248
- model,
2249
- inputs: documents.map((doc) => doc.text)
3479
+ async startSession(config) {
3480
+ return new MistralConversationSession({
3481
+ sessionConfig: {
3482
+ ...config,
3483
+ voiceId: config.voiceId || this.defaultVoiceId
3484
+ },
3485
+ defaultModel: this.defaultModel,
3486
+ complete: (history, sessionConfig) => this.completeConversation(history, sessionConfig),
3487
+ sttProvider: this.sttProvider
2250
3488
  });
2251
- return response.data.map((item, index) => ({
2252
- id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
2253
- vector: item.embedding ?? [],
2254
- dimensions: item.embedding?.length ?? 0,
2255
- model: response.model,
2256
- metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
2257
- }));
2258
3489
  }
2259
- async embedQuery(query, options) {
2260
- const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
2261
- if (!result) {
2262
- throw new Error("Failed to compute embedding for query");
3490
+ async listVoices() {
3491
+ return [
3492
+ {
3493
+ id: this.defaultVoiceId,
3494
+ name: "Mistral Default Voice",
3495
+ description: "Default conversational voice profile.",
3496
+ capabilities: ["conversational"]
3497
+ }
3498
+ ];
3499
+ }
3500
+ async completeConversation(history, sessionConfig) {
3501
+ const model = sessionConfig.llmModel ?? this.defaultModel;
3502
+ const messages = [];
3503
+ if (sessionConfig.systemPrompt) {
3504
+ messages.push({ role: "system", content: sessionConfig.systemPrompt });
2263
3505
  }
2264
- return result;
3506
+ for (const item of history) {
3507
+ messages.push({ role: item.role, content: item.content });
3508
+ }
3509
+ const response = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
3510
+ method: "POST",
3511
+ headers: {
3512
+ Authorization: `Bearer ${this.apiKey}`,
3513
+ "Content-Type": "application/json"
3514
+ },
3515
+ body: JSON.stringify({
3516
+ model,
3517
+ messages
3518
+ })
3519
+ });
3520
+ if (!response.ok) {
3521
+ const body = await response.text();
3522
+ throw new Error(`Mistral conversational request failed (${response.status}): ${body}`);
3523
+ }
3524
+ const payload = await response.json();
3525
+ return readAssistantText(payload);
3526
+ }
3527
+ }
3528
+ function normalizeBaseUrl2(url) {
3529
+ return url.endsWith("/") ? url.slice(0, -1) : url;
3530
+ }
3531
+ function readAssistantText(payload) {
3532
+ const record = asRecord3(payload);
3533
+ const choices = Array.isArray(record.choices) ? record.choices : [];
3534
+ const firstChoice = asRecord3(choices[0]);
3535
+ const message = asRecord3(firstChoice.message);
3536
+ if (typeof message.content === "string") {
3537
+ return message.content;
3538
+ }
3539
+ if (Array.isArray(message.content)) {
3540
+ const textParts = message.content.map((part) => {
3541
+ const entry = asRecord3(part);
3542
+ const text = entry.text;
3543
+ return typeof text === "string" ? text : "";
3544
+ }).filter((text) => text.length > 0);
3545
+ return textParts.join("");
2265
3546
  }
3547
+ return "";
3548
+ }
3549
+ function asRecord3(value) {
3550
+ if (value && typeof value === "object") {
3551
+ return value;
3552
+ }
3553
+ return {};
2266
3554
  }
2267
3555
 
2268
3556
  // src/impls/qdrant-vector.ts
@@ -2664,7 +3952,7 @@ function distanceToScore(distance, metric) {
2664
3952
 
2665
3953
  // src/impls/stripe-payments.ts
2666
3954
  import Stripe from "stripe";
2667
- var API_VERSION = "2026-01-28.clover";
3955
+ var API_VERSION = "2026-02-25.clover";
2668
3956
 
2669
3957
  class StripePaymentsProvider {
2670
3958
  stripe;
@@ -3329,6 +4617,318 @@ function mapStatus(status) {
3329
4617
  }
3330
4618
  }
3331
4619
 
4620
+ // src/impls/messaging-slack.ts
4621
+ class SlackMessagingProvider {
4622
+ botToken;
4623
+ defaultChannelId;
4624
+ apiBaseUrl;
4625
+ constructor(options) {
4626
+ this.botToken = options.botToken;
4627
+ this.defaultChannelId = options.defaultChannelId;
4628
+ this.apiBaseUrl = options.apiBaseUrl ?? "https://slack.com/api";
4629
+ }
4630
+ async sendMessage(input) {
4631
+ const channel = input.channelId ?? input.recipientId ?? this.defaultChannelId;
4632
+ if (!channel) {
4633
+ throw new Error("Slack sendMessage requires channelId, recipientId, or defaultChannelId.");
4634
+ }
4635
+ const payload = {
4636
+ channel,
4637
+ text: input.text,
4638
+ mrkdwn: input.markdown ?? true,
4639
+ thread_ts: input.threadId
4640
+ };
4641
+ const response = await fetch(`${this.apiBaseUrl}/chat.postMessage`, {
4642
+ method: "POST",
4643
+ headers: {
4644
+ authorization: `Bearer ${this.botToken}`,
4645
+ "content-type": "application/json"
4646
+ },
4647
+ body: JSON.stringify(payload)
4648
+ });
4649
+ const body = await response.json();
4650
+ if (!response.ok || !body.ok || !body.ts) {
4651
+ throw new Error(`Slack sendMessage failed: ${body.error ?? `HTTP_${response.status}`}`);
4652
+ }
4653
+ return {
4654
+ id: `slack:${body.channel ?? channel}:${body.ts}`,
4655
+ providerMessageId: body.ts,
4656
+ status: "sent",
4657
+ sentAt: new Date,
4658
+ metadata: {
4659
+ channelId: body.channel ?? channel
4660
+ }
4661
+ };
4662
+ }
4663
+ async updateMessage(messageId, input) {
4664
+ const channel = input.channelId ?? this.defaultChannelId;
4665
+ if (!channel) {
4666
+ throw new Error("Slack updateMessage requires channelId or defaultChannelId.");
4667
+ }
4668
+ const response = await fetch(`${this.apiBaseUrl}/chat.update`, {
4669
+ method: "POST",
4670
+ headers: {
4671
+ authorization: `Bearer ${this.botToken}`,
4672
+ "content-type": "application/json"
4673
+ },
4674
+ body: JSON.stringify({
4675
+ channel,
4676
+ ts: messageId,
4677
+ text: input.text,
4678
+ mrkdwn: input.markdown ?? true
4679
+ })
4680
+ });
4681
+ const body = await response.json();
4682
+ if (!response.ok || !body.ok || !body.ts) {
4683
+ throw new Error(`Slack updateMessage failed: ${body.error ?? `HTTP_${response.status}`}`);
4684
+ }
4685
+ return {
4686
+ id: `slack:${body.channel ?? channel}:${body.ts}`,
4687
+ providerMessageId: body.ts,
4688
+ status: "sent",
4689
+ sentAt: new Date,
4690
+ metadata: {
4691
+ channelId: body.channel ?? channel
4692
+ }
4693
+ };
4694
+ }
4695
+ }
4696
+
4697
+ // src/impls/messaging-github.ts
4698
+ class GithubMessagingProvider {
4699
+ token;
4700
+ defaultOwner;
4701
+ defaultRepo;
4702
+ apiBaseUrl;
4703
+ constructor(options) {
4704
+ this.token = options.token;
4705
+ this.defaultOwner = options.defaultOwner;
4706
+ this.defaultRepo = options.defaultRepo;
4707
+ this.apiBaseUrl = options.apiBaseUrl ?? "https://api.github.com";
4708
+ }
4709
+ async sendMessage(input) {
4710
+ const target = this.resolveTarget(input);
4711
+ const response = await fetch(`${this.apiBaseUrl}/repos/${target.owner}/${target.repo}/issues/${target.issueNumber}/comments`, {
4712
+ method: "POST",
4713
+ headers: {
4714
+ authorization: `Bearer ${this.token}`,
4715
+ accept: "application/vnd.github+json",
4716
+ "content-type": "application/json"
4717
+ },
4718
+ body: JSON.stringify({ body: input.text })
4719
+ });
4720
+ const body = await response.json();
4721
+ if (!response.ok || !body.id) {
4722
+ throw new Error(`GitHub sendMessage failed: ${body.message ?? `HTTP_${response.status}`}`);
4723
+ }
4724
+ return {
4725
+ id: String(body.id),
4726
+ providerMessageId: body.node_id,
4727
+ status: "sent",
4728
+ sentAt: new Date,
4729
+ metadata: {
4730
+ url: body.html_url ?? "",
4731
+ owner: target.owner,
4732
+ repo: target.repo,
4733
+ issueNumber: String(target.issueNumber)
4734
+ }
4735
+ };
4736
+ }
4737
+ async updateMessage(messageId, input) {
4738
+ const owner = input.metadata?.owner ?? this.defaultOwner;
4739
+ const repo = input.metadata?.repo ?? this.defaultRepo;
4740
+ if (!owner || !repo) {
4741
+ throw new Error("GitHub updateMessage requires owner and repo metadata.");
4742
+ }
4743
+ const response = await fetch(`${this.apiBaseUrl}/repos/${owner}/${repo}/issues/comments/${messageId}`, {
4744
+ method: "PATCH",
4745
+ headers: {
4746
+ authorization: `Bearer ${this.token}`,
4747
+ accept: "application/vnd.github+json",
4748
+ "content-type": "application/json"
4749
+ },
4750
+ body: JSON.stringify({ body: input.text })
4751
+ });
4752
+ const body = await response.json();
4753
+ if (!response.ok || !body.id) {
4754
+ throw new Error(`GitHub updateMessage failed: ${body.message ?? `HTTP_${response.status}`}`);
4755
+ }
4756
+ return {
4757
+ id: String(body.id),
4758
+ providerMessageId: body.node_id,
4759
+ status: "sent",
4760
+ sentAt: new Date,
4761
+ metadata: {
4762
+ url: body.html_url ?? "",
4763
+ owner,
4764
+ repo
4765
+ }
4766
+ };
4767
+ }
4768
+ resolveTarget(input) {
4769
+ const parsedRecipient = parseRecipient(input.recipientId);
4770
+ const owner = parsedRecipient?.owner ?? this.defaultOwner;
4771
+ const repo = parsedRecipient?.repo ?? this.defaultRepo;
4772
+ const issueNumber = parsedRecipient?.issueNumber ?? parseIssueNumber(input.threadId);
4773
+ if (!owner || !repo || issueNumber == null) {
4774
+ throw new Error("GitHub sendMessage requires owner/repo and issueNumber (use recipientId like owner/repo#123 or provide defaults + threadId).");
4775
+ }
4776
+ return {
4777
+ owner,
4778
+ repo,
4779
+ issueNumber
4780
+ };
4781
+ }
4782
+ }
4783
+ function parseRecipient(value) {
4784
+ if (!value)
4785
+ return null;
4786
+ const match = value.trim().match(/^([^/]+)\/([^#]+)#(\d+)$/);
4787
+ if (!match)
4788
+ return null;
4789
+ const owner = match[1];
4790
+ const repo = match[2];
4791
+ const issueNumber = Number(match[3]);
4792
+ if (!owner || !repo || !Number.isInteger(issueNumber)) {
4793
+ return null;
4794
+ }
4795
+ return { owner, repo, issueNumber };
4796
+ }
4797
+ function parseIssueNumber(value) {
4798
+ if (!value)
4799
+ return null;
4800
+ const numeric = Number(value);
4801
+ return Number.isInteger(numeric) ? numeric : null;
4802
+ }
4803
+
4804
+ // src/impls/messaging-whatsapp-meta.ts
4805
+ class MetaWhatsappMessagingProvider {
4806
+ accessToken;
4807
+ phoneNumberId;
4808
+ apiVersion;
4809
+ constructor(options) {
4810
+ this.accessToken = options.accessToken;
4811
+ this.phoneNumberId = options.phoneNumberId;
4812
+ this.apiVersion = options.apiVersion ?? "v22.0";
4813
+ }
4814
+ async sendMessage(input) {
4815
+ const to = input.recipientId;
4816
+ if (!to) {
4817
+ throw new Error("Meta WhatsApp sendMessage requires recipientId.");
4818
+ }
4819
+ const response = await fetch(`https://graph.facebook.com/${this.apiVersion}/${this.phoneNumberId}/messages`, {
4820
+ method: "POST",
4821
+ headers: {
4822
+ authorization: `Bearer ${this.accessToken}`,
4823
+ "content-type": "application/json"
4824
+ },
4825
+ body: JSON.stringify({
4826
+ messaging_product: "whatsapp",
4827
+ to,
4828
+ type: "text",
4829
+ text: {
4830
+ body: input.text,
4831
+ preview_url: false
4832
+ }
4833
+ })
4834
+ });
4835
+ const body = await response.json();
4836
+ const messageId = body.messages?.[0]?.id;
4837
+ if (!response.ok || !messageId) {
4838
+ const errorCode = body.error?.code != null ? String(body.error.code) : "";
4839
+ throw new Error(`Meta WhatsApp sendMessage failed: ${body.error?.message ?? `HTTP_${response.status}`}${errorCode ? ` (${errorCode})` : ""}`);
4840
+ }
4841
+ return {
4842
+ id: messageId,
4843
+ providerMessageId: messageId,
4844
+ status: "sent",
4845
+ sentAt: new Date,
4846
+ metadata: {
4847
+ phoneNumberId: this.phoneNumberId
4848
+ }
4849
+ };
4850
+ }
4851
+ }
4852
+
4853
+ // src/impls/messaging-whatsapp-twilio.ts
4854
+ import { Buffer as Buffer4 } from "buffer";
4855
+
4856
+ class TwilioWhatsappMessagingProvider {
4857
+ accountSid;
4858
+ authToken;
4859
+ fromNumber;
4860
+ constructor(options) {
4861
+ this.accountSid = options.accountSid;
4862
+ this.authToken = options.authToken;
4863
+ this.fromNumber = options.fromNumber;
4864
+ }
4865
+ async sendMessage(input) {
4866
+ const to = normalizeWhatsappAddress(input.recipientId);
4867
+ const from = normalizeWhatsappAddress(input.channelId ?? this.fromNumber);
4868
+ if (!to) {
4869
+ throw new Error("Twilio WhatsApp sendMessage requires recipientId.");
4870
+ }
4871
+ if (!from) {
4872
+ throw new Error("Twilio WhatsApp sendMessage requires channelId or configured fromNumber.");
4873
+ }
4874
+ const params = new URLSearchParams;
4875
+ params.set("To", to);
4876
+ params.set("From", from);
4877
+ params.set("Body", input.text);
4878
+ const auth = Buffer4.from(`${this.accountSid}:${this.authToken}`).toString("base64");
4879
+ const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`, {
4880
+ method: "POST",
4881
+ headers: {
4882
+ authorization: `Basic ${auth}`,
4883
+ "content-type": "application/x-www-form-urlencoded"
4884
+ },
4885
+ body: params.toString()
4886
+ });
4887
+ const body = await response.json();
4888
+ if (!response.ok || !body.sid) {
4889
+ throw new Error(`Twilio WhatsApp sendMessage failed: ${body.error_message ?? `HTTP_${response.status}`}`);
4890
+ }
4891
+ return {
4892
+ id: body.sid,
4893
+ providerMessageId: body.sid,
4894
+ status: mapTwilioStatus(body.status),
4895
+ sentAt: new Date,
4896
+ errorCode: body.error_code != null ? String(body.error_code) : undefined,
4897
+ errorMessage: body.error_message ?? undefined,
4898
+ metadata: {
4899
+ from,
4900
+ to
4901
+ }
4902
+ };
4903
+ }
4904
+ }
4905
+ function normalizeWhatsappAddress(value) {
4906
+ if (!value)
4907
+ return null;
4908
+ if (value.startsWith("whatsapp:"))
4909
+ return value;
4910
+ return `whatsapp:${value}`;
4911
+ }
4912
+ function mapTwilioStatus(status) {
4913
+ switch (status) {
4914
+ case "queued":
4915
+ case "accepted":
4916
+ case "scheduled":
4917
+ return "queued";
4918
+ case "sending":
4919
+ return "sending";
4920
+ case "delivered":
4921
+ return "delivered";
4922
+ case "failed":
4923
+ case "undelivered":
4924
+ case "canceled":
4925
+ return "failed";
4926
+ case "sent":
4927
+ default:
4928
+ return "sent";
4929
+ }
4930
+ }
4931
+
3332
4932
  // src/impls/powens-client.ts
3333
4933
  import { URL as URL2 } from "url";
3334
4934
  var POWENS_BASE_URL = {
@@ -3835,7 +5435,7 @@ function resolveLabelIds(defaults, tags) {
3835
5435
  }
3836
5436
 
3837
5437
  // src/impls/jira.ts
3838
- import { Buffer as Buffer4 } from "buffer";
5438
+ import { Buffer as Buffer5 } from "buffer";
3839
5439
 
3840
5440
  class JiraProjectManagementProvider {
3841
5441
  siteUrl;
@@ -3912,7 +5512,7 @@ function normalizeSiteUrl(siteUrl) {
3912
5512
  return siteUrl.replace(/\/$/, "");
3913
5513
  }
3914
5514
  function buildAuthHeader(email, apiToken) {
3915
- const token = Buffer4.from(`${email}:${apiToken}`).toString("base64");
5515
+ const token = Buffer5.from(`${email}:${apiToken}`).toString("base64");
3916
5516
  return `Basic ${token}`;
3917
5517
  }
3918
5518
  function resolveIssueType(type, defaults) {
@@ -4115,7 +5715,7 @@ function buildParagraphBlocks(text) {
4115
5715
  }
4116
5716
 
4117
5717
  // src/impls/tldv-meeting-recorder.ts
4118
- var DEFAULT_BASE_URL4 = "https://pasta.tldv.io/v1alpha1";
5718
+ var DEFAULT_BASE_URL6 = "https://pasta.tldv.io/v1alpha1";
4119
5719
 
4120
5720
  class TldvMeetingRecorderProvider {
4121
5721
  apiKey;
@@ -4123,7 +5723,7 @@ class TldvMeetingRecorderProvider {
4123
5723
  defaultPageSize;
4124
5724
  constructor(options) {
4125
5725
  this.apiKey = options.apiKey;
4126
- this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL4;
5726
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL6;
4127
5727
  this.defaultPageSize = options.pageSize;
4128
5728
  }
4129
5729
  async listMeetings(params) {
@@ -4258,10 +5858,30 @@ async function safeReadError4(response) {
4258
5858
  }
4259
5859
 
4260
5860
  // src/impls/provider-factory.ts
4261
- import { Buffer as Buffer5 } from "buffer";
5861
+ import { Buffer as Buffer6 } from "buffer";
5862
+ import { resolveIntegrationRequestContext } from "@contractspec/lib.contracts-integrations/integrations/runtime";
5863
+ import { buildAuthHeaders } from "@contractspec/lib.contracts-integrations/integrations/auth-helpers";
5864
+ import { findAuthConfig } from "@contractspec/lib.contracts-integrations/integrations/auth";
4262
5865
  var SECRET_CACHE = new Map;
4263
5866
 
4264
5867
  class IntegrationProviderFactory {
5868
+ composioFallback;
5869
+ constructor(options) {
5870
+ this.composioFallback = options?.composioFallback;
5871
+ }
5872
+ async resolveProviderContext(context) {
5873
+ const secrets = await this.loadSecrets(context);
5874
+ const { transport, authMethod, apiVersion } = resolveIntegrationRequestContext(context.spec, context.connection);
5875
+ let authHeaders = {};
5876
+ if (authMethod && context.spec.supportedAuthMethods) {
5877
+ const authConfig = findAuthConfig(context.spec.supportedAuthMethods, authMethod);
5878
+ if (authConfig) {
5879
+ const stringSecrets = Object.fromEntries(Object.entries(secrets).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v]));
5880
+ authHeaders = buildAuthHeaders(authConfig, stringSecrets);
5881
+ }
5882
+ }
5883
+ return { transport, authMethod, apiVersion, authHeaders, secrets };
5884
+ }
4265
5885
  async createPaymentsProvider(context) {
4266
5886
  const secrets = await this.loadSecrets(context);
4267
5887
  switch (context.spec.meta.key) {
@@ -4270,6 +5890,9 @@ class IntegrationProviderFactory {
4270
5890
  apiKey: requireSecret(secrets, "apiKey", "Stripe API key is required")
4271
5891
  });
4272
5892
  default:
5893
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
5894
+ return this.composioFallback.createPaymentsProxy(context);
5895
+ }
4273
5896
  throw new Error(`Unsupported payments integration: ${context.spec.meta.key}`);
4274
5897
  }
4275
5898
  }
@@ -4283,6 +5906,9 @@ class IntegrationProviderFactory {
4283
5906
  messageStream: context.config.messageStream
4284
5907
  });
4285
5908
  default:
5909
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
5910
+ return this.composioFallback.createEmailProxy(context);
5911
+ }
4286
5912
  throw new Error(`Unsupported email integration: ${context.spec.meta.key}`);
4287
5913
  }
4288
5914
  }
@@ -4296,9 +5922,48 @@ class IntegrationProviderFactory {
4296
5922
  fromNumber: context.config.fromNumber
4297
5923
  });
4298
5924
  default:
5925
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
5926
+ return this.composioFallback.createMessagingProxy(context);
5927
+ }
4299
5928
  throw new Error(`Unsupported SMS integration: ${context.spec.meta.key}`);
4300
5929
  }
4301
5930
  }
5931
+ async createMessagingProvider(context) {
5932
+ const secrets = await this.loadSecrets(context);
5933
+ const config = context.config;
5934
+ switch (context.spec.meta.key) {
5935
+ case "messaging.slack":
5936
+ return new SlackMessagingProvider({
5937
+ botToken: requireSecret(secrets, "botToken", "Slack bot token is required"),
5938
+ defaultChannelId: config?.defaultChannelId,
5939
+ apiBaseUrl: config?.apiBaseUrl
5940
+ });
5941
+ case "messaging.github":
5942
+ return new GithubMessagingProvider({
5943
+ token: requireSecret(secrets, "token", "GitHub token is required"),
5944
+ defaultOwner: config?.defaultOwner,
5945
+ defaultRepo: config?.defaultRepo,
5946
+ apiBaseUrl: config?.apiBaseUrl
5947
+ });
5948
+ case "messaging.whatsapp.meta":
5949
+ return new MetaWhatsappMessagingProvider({
5950
+ accessToken: requireSecret(secrets, "accessToken", "Meta WhatsApp access token is required"),
5951
+ phoneNumberId: requireConfig(context, "phoneNumberId", "Meta WhatsApp phoneNumberId is required"),
5952
+ apiVersion: config?.apiVersion
5953
+ });
5954
+ case "messaging.whatsapp.twilio":
5955
+ return new TwilioWhatsappMessagingProvider({
5956
+ accountSid: requireSecret(secrets, "accountSid", "Twilio account SID is required"),
5957
+ authToken: requireSecret(secrets, "authToken", "Twilio auth token is required"),
5958
+ fromNumber: config?.fromNumber
5959
+ });
5960
+ default:
5961
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
5962
+ return this.composioFallback.createMessagingProxy(context);
5963
+ }
5964
+ throw new Error(`Unsupported messaging integration: ${context.spec.meta.key}`);
5965
+ }
5966
+ }
4302
5967
  async createVectorStoreProvider(context) {
4303
5968
  const secrets = await this.loadSecrets(context);
4304
5969
  const config = context.config;
@@ -4319,6 +5984,9 @@ class IntegrationProviderFactory {
4319
5984
  sslMode: config?.sslMode
4320
5985
  });
4321
5986
  default:
5987
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
5988
+ return this.composioFallback.createGenericProxy(context);
5989
+ }
4322
5990
  throw new Error(`Unsupported vector store integration: ${context.spec.meta.key}`);
4323
5991
  }
4324
5992
  }
@@ -4335,6 +6003,9 @@ class IntegrationProviderFactory {
4335
6003
  personalApiKey: requireSecret(secrets, "personalApiKey", "PostHog personalApiKey is required")
4336
6004
  });
4337
6005
  default:
6006
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6007
+ return this.composioFallback.createGenericProxy(context);
6008
+ }
4338
6009
  throw new Error(`Unsupported analytics integration: ${context.spec.meta.key}`);
4339
6010
  }
4340
6011
  }
@@ -4349,6 +6020,9 @@ class IntegrationProviderFactory {
4349
6020
  sslMode: config?.sslMode
4350
6021
  });
4351
6022
  default:
6023
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6024
+ return this.composioFallback.createGenericProxy(context);
6025
+ }
4352
6026
  throw new Error(`Unsupported database integration: ${context.spec.meta.key}`);
4353
6027
  }
4354
6028
  }
@@ -4362,6 +6036,9 @@ class IntegrationProviderFactory {
4362
6036
  clientOptions: secrets.type === "service_account" ? { credentials: secrets } : undefined
4363
6037
  });
4364
6038
  default:
6039
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6040
+ return this.composioFallback.createGenericProxy(context);
6041
+ }
4365
6042
  throw new Error(`Unsupported storage integration: ${context.spec.meta.key}`);
4366
6043
  }
4367
6044
  }
@@ -4394,9 +6071,53 @@ class IntegrationProviderFactory {
4394
6071
  pollIntervalMs: config?.pollIntervalMs
4395
6072
  });
4396
6073
  default:
6074
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6075
+ return this.composioFallback.createGenericProxy(context);
6076
+ }
4397
6077
  throw new Error(`Unsupported voice integration: ${context.spec.meta.key}`);
4398
6078
  }
4399
6079
  }
6080
+ async createSttProvider(context) {
6081
+ const secrets = await this.loadSecrets(context);
6082
+ const config = context.config;
6083
+ switch (context.spec.meta.key) {
6084
+ case "ai-voice-stt.mistral":
6085
+ return new MistralSttProvider({
6086
+ apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
6087
+ defaultModel: config?.model,
6088
+ defaultLanguage: config?.language,
6089
+ serverURL: config?.serverURL
6090
+ });
6091
+ default:
6092
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6093
+ return this.composioFallback.createGenericProxy(context);
6094
+ }
6095
+ throw new Error(`Unsupported STT integration: ${context.spec.meta.key}`);
6096
+ }
6097
+ }
6098
+ async createConversationalProvider(context) {
6099
+ const secrets = await this.loadSecrets(context);
6100
+ const config = context.config;
6101
+ switch (context.spec.meta.key) {
6102
+ case "ai-voice-conv.mistral":
6103
+ return new MistralConversationalProvider({
6104
+ apiKey: requireSecret(secrets, "apiKey", "Mistral API key is required"),
6105
+ defaultModel: config?.model,
6106
+ defaultVoiceId: config?.defaultVoice,
6107
+ serverURL: config?.serverURL,
6108
+ sttOptions: {
6109
+ defaultModel: config?.model,
6110
+ defaultLanguage: config?.language,
6111
+ serverURL: config?.serverURL
6112
+ }
6113
+ });
6114
+ default:
6115
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6116
+ return this.composioFallback.createGenericProxy(context);
6117
+ }
6118
+ throw new Error(`Unsupported conversational integration: ${context.spec.meta.key}`);
6119
+ }
6120
+ }
4400
6121
  async createProjectManagementProvider(context) {
4401
6122
  const secrets = await this.loadSecrets(context);
4402
6123
  const config = context.config;
@@ -4434,6 +6155,9 @@ class IntegrationProviderFactory {
4434
6155
  descriptionProperty: config?.descriptionProperty
4435
6156
  });
4436
6157
  default:
6158
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6159
+ return this.composioFallback.createProjectManagementProxy(context);
6160
+ }
4437
6161
  throw new Error(`Unsupported project management integration: ${context.spec.meta.key}`);
4438
6162
  }
4439
6163
  }
@@ -4482,6 +6206,9 @@ class IntegrationProviderFactory {
4482
6206
  webhookSecret: secrets.webhookSecret
4483
6207
  });
4484
6208
  default:
6209
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6210
+ return this.composioFallback.createGenericProxy(context);
6211
+ }
4485
6212
  throw new Error(`Unsupported meeting recorder integration: ${context.spec.meta.key}`);
4486
6213
  }
4487
6214
  }
@@ -4494,6 +6221,9 @@ class IntegrationProviderFactory {
4494
6221
  defaultModel: context.config.model
4495
6222
  });
4496
6223
  default:
6224
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6225
+ return this.composioFallback.createGenericProxy(context);
6226
+ }
4497
6227
  throw new Error(`Unsupported LLM integration: ${context.spec.meta.key}`);
4498
6228
  }
4499
6229
  }
@@ -4506,6 +6236,9 @@ class IntegrationProviderFactory {
4506
6236
  defaultModel: context.config.embeddingModel
4507
6237
  });
4508
6238
  default:
6239
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6240
+ return this.composioFallback.createGenericProxy(context);
6241
+ }
4509
6242
  throw new Error(`Unsupported embeddings integration: ${context.spec.meta.key}`);
4510
6243
  }
4511
6244
  }
@@ -4527,6 +6260,9 @@ class IntegrationProviderFactory {
4527
6260
  });
4528
6261
  }
4529
6262
  default:
6263
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6264
+ return this.composioFallback.createGenericProxy(context);
6265
+ }
4530
6266
  throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
4531
6267
  }
4532
6268
  }
@@ -4547,7 +6283,7 @@ class IntegrationProviderFactory {
4547
6283
  }
4548
6284
  }
4549
6285
  function parseSecret(secret) {
4550
- const text = Buffer5.from(secret.data).toString("utf-8").trim();
6286
+ const text = Buffer6.from(secret.data).toString("utf-8").trim();
4551
6287
  if (!text)
4552
6288
  return {};
4553
6289
  try {