@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,3 +1,50 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
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
+
1
48
  // src/impls/elevenlabs-voice.ts
2
49
  import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
3
50
  var FORMAT_MAP = {
@@ -1464,7 +1511,286 @@ async function safeReadError3(response) {
1464
1511
  }
1465
1512
  }
1466
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
+
1467
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
+
1468
1794
  class BaseHealthProvider {
1469
1795
  providerKey;
1470
1796
  transport;
@@ -1472,146 +1798,191 @@ class BaseHealthProvider {
1472
1798
  mcpUrl;
1473
1799
  apiKey;
1474
1800
  accessToken;
1801
+ refreshToken;
1475
1802
  mcpAccessToken;
1476
1803
  webhookSecret;
1804
+ webhookSignatureHeader;
1805
+ route;
1806
+ aggregatorKey;
1807
+ oauth;
1477
1808
  fetchFn;
1478
1809
  mcpRequestId = 0;
1479
1810
  constructor(options) {
1480
1811
  this.providerKey = options.providerKey;
1481
1812
  this.transport = options.transport;
1482
- this.apiBaseUrl = options.apiBaseUrl ?? "https://api.example-health.local";
1813
+ this.apiBaseUrl = options.apiBaseUrl;
1483
1814
  this.mcpUrl = options.mcpUrl;
1484
1815
  this.apiKey = options.apiKey;
1485
1816
  this.accessToken = options.accessToken;
1817
+ this.refreshToken = options.oauth?.refreshToken;
1486
1818
  this.mcpAccessToken = options.mcpAccessToken;
1487
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 ?? {};
1488
1824
  this.fetchFn = options.fetchFn ?? fetch;
1489
1825
  }
1490
- async listActivities(params) {
1491
- const result = await this.fetchList("activities", params);
1492
- return {
1493
- activities: result.items,
1494
- nextCursor: result.nextCursor,
1495
- hasMore: result.hasMore,
1496
- source: this.currentSource()
1497
- };
1826
+ async listActivities(_params) {
1827
+ throw this.unsupported("activities");
1498
1828
  }
1499
- async listWorkouts(params) {
1500
- 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);
1501
1874
  return {
1502
- workouts: result.items,
1503
- nextCursor: result.nextCursor,
1504
- hasMore: result.hasMore,
1875
+ activities: response.items,
1876
+ nextCursor: response.nextCursor,
1877
+ hasMore: response.hasMore,
1505
1878
  source: this.currentSource()
1506
1879
  };
1507
1880
  }
1508
- async listSleep(params) {
1509
- const result = await this.fetchList("sleep", params);
1881
+ async fetchWorkouts(params, config) {
1882
+ const response = await this.fetchList(params, config);
1510
1883
  return {
1511
- sleep: result.items,
1512
- nextCursor: result.nextCursor,
1513
- hasMore: result.hasMore,
1884
+ workouts: response.items,
1885
+ nextCursor: response.nextCursor,
1886
+ hasMore: response.hasMore,
1514
1887
  source: this.currentSource()
1515
1888
  };
1516
1889
  }
1517
- async listBiometrics(params) {
1518
- const result = await this.fetchList("biometrics", params);
1890
+ async fetchSleep(params, config) {
1891
+ const response = await this.fetchList(params, config);
1519
1892
  return {
1520
- biometrics: result.items,
1521
- nextCursor: result.nextCursor,
1522
- hasMore: result.hasMore,
1893
+ sleep: response.items,
1894
+ nextCursor: response.nextCursor,
1895
+ hasMore: response.hasMore,
1523
1896
  source: this.currentSource()
1524
1897
  };
1525
1898
  }
1526
- async listNutrition(params) {
1527
- const result = await this.fetchList("nutrition", params);
1899
+ async fetchBiometrics(params, config) {
1900
+ const response = await this.fetchList(params, config);
1528
1901
  return {
1529
- nutrition: result.items,
1530
- nextCursor: result.nextCursor,
1531
- hasMore: result.hasMore,
1902
+ biometrics: response.items,
1903
+ nextCursor: response.nextCursor,
1904
+ hasMore: response.hasMore,
1532
1905
  source: this.currentSource()
1533
1906
  };
1534
1907
  }
1535
- async getConnectionStatus(params) {
1536
- const payload = await this.fetchRecord("connection/status", params);
1537
- const status = readString2(payload, "status") ?? "healthy";
1908
+ async fetchNutrition(params, config) {
1909
+ const response = await this.fetchList(params, config);
1538
1910
  return {
1539
- tenantId: params.tenantId,
1540
- connectionId: params.connectionId,
1541
- status: status === "healthy" || status === "degraded" || status === "error" || status === "disconnected" ? status : "healthy",
1542
- source: this.currentSource(),
1543
- lastCheckedAt: readString2(payload, "lastCheckedAt") ?? new Date().toISOString(),
1544
- errorCode: readString2(payload, "errorCode"),
1545
- errorMessage: readString2(payload, "errorMessage"),
1546
- metadata: asRecord(payload.metadata)
1911
+ nutrition: response.items,
1912
+ nextCursor: response.nextCursor,
1913
+ hasMore: response.hasMore,
1914
+ source: this.currentSource()
1547
1915
  };
1548
1916
  }
1549
- async syncActivities(params) {
1550
- return this.sync("activities", params);
1551
- }
1552
- async syncWorkouts(params) {
1553
- return this.sync("workouts", params);
1554
- }
1555
- async syncSleep(params) {
1556
- return this.sync("sleep", params);
1557
- }
1558
- async syncBiometrics(params) {
1559
- return this.sync("biometrics", params);
1560
- }
1561
- async syncNutrition(params) {
1562
- 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());
1563
1920
  }
1564
- async parseWebhook(request) {
1565
- const payload = request.parsedBody ?? safeJsonParse(request.rawBody);
1566
- const body = asRecord(payload);
1921
+ currentSource() {
1567
1922
  return {
1568
1923
  providerKey: this.providerKey,
1569
- eventType: readString2(body, "eventType") ?? readString2(body, "event"),
1570
- externalEntityId: readString2(body, "externalEntityId") ?? readString2(body, "entityId"),
1571
- entityType: normalizeEntityType(readString2(body, "entityType") ?? readString2(body, "type")),
1572
- receivedAt: new Date().toISOString(),
1573
- verified: await this.verifyWebhook(request),
1574
- payload
1924
+ transport: this.transport,
1925
+ route: this.route,
1926
+ aggregatorKey: this.aggregatorKey
1575
1927
  };
1576
1928
  }
1577
- async verifyWebhook(request) {
1578
- if (!this.webhookSecret) {
1579
- return true;
1580
- }
1581
- const signature = readHeader(request.headers, "x-webhook-signature");
1582
- 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}`);
1583
1934
  }
1584
- async fetchList(resource, params) {
1585
- const payload = await this.fetchRecord(resource, params);
1586
- 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);
1587
1938
  return {
1588
- items,
1589
- nextCursor: readString2(payload, "nextCursor") ?? readString2(payload, "cursor"),
1590
- hasMore: readBoolean2(payload, "hasMore")
1939
+ synced: records,
1940
+ failed: 0,
1941
+ nextCursor: undefined,
1942
+ source: result.source
1591
1943
  };
1592
1944
  }
1593
- async sync(resource, params) {
1594
- 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);
1595
1949
  return {
1596
- synced: readNumber(payload, "synced") ?? 0,
1597
- failed: readNumber(payload, "failed") ?? 0,
1598
- nextCursor: readString2(payload, "nextCursor"),
1599
- errors: asArray2(payload.errors)?.map((item) => String(item)),
1600
- source: this.currentSource()
1950
+ items,
1951
+ nextCursor: pagination.nextCursor,
1952
+ hasMore: pagination.hasMore
1601
1953
  };
1602
1954
  }
1603
- async fetchRecord(resource, params, method = "GET") {
1604
- if (this.transport.endsWith("mcp")) {
1605
- 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);
1606
1970
  }
1607
- const url = new URL(`${this.apiBaseUrl.replace(/\/$/, "")}/${resource}`);
1608
- if (method === "GET") {
1609
- 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)) {
1610
1980
  if (value == null)
1611
1981
  continue;
1612
1982
  if (Array.isArray(value)) {
1613
- value.forEach((item) => {
1614
- url.searchParams.append(key, String(item));
1983
+ value.forEach((entry) => {
1984
+ if (entry != null)
1985
+ url.searchParams.append(key, String(entry));
1615
1986
  });
1616
1987
  continue;
1617
1988
  }
@@ -1620,22 +1991,22 @@ class BaseHealthProvider {
1620
1991
  }
1621
1992
  const response = await this.fetchFn(url, {
1622
1993
  method,
1623
- headers: {
1624
- "Content-Type": "application/json",
1625
- ...this.accessToken || this.apiKey ? { Authorization: `Bearer ${this.accessToken ?? this.apiKey}` } : {}
1626
- },
1627
- body: method === "POST" ? JSON.stringify(params) : undefined
1994
+ headers: this.authorizationHeaders(),
1995
+ body: method === "POST" ? JSON.stringify(body ?? {}) : undefined
1628
1996
  });
1629
- if (!response.ok) {
1630
- const errorBody = await safeResponseText(response);
1631
- 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);
1632
2004
  }
1633
- const data = await response.json();
1634
- return asRecord(data) ?? {};
2005
+ return this.readResponsePayload(response, path);
1635
2006
  }
1636
- async callMcpTool(resource, params) {
2007
+ async callMcpTool(toolName, args) {
1637
2008
  if (!this.mcpUrl) {
1638
- return {};
2009
+ throw new Error(`${this.providerKey} MCP URL is not configured.`);
1639
2010
  }
1640
2011
  const response = await this.fetchFn(this.mcpUrl, {
1641
2012
  method: "POST",
@@ -1648,78 +2019,103 @@ class BaseHealthProvider {
1648
2019
  id: ++this.mcpRequestId,
1649
2020
  method: "tools/call",
1650
2021
  params: {
1651
- name: `${this.providerKey.replace("health.", "")}_${resource.replace(/\//g, "_")}`,
1652
- arguments: params
2022
+ name: toolName,
2023
+ arguments: args
1653
2024
  }
1654
2025
  })
1655
2026
  });
1656
- if (!response.ok) {
1657
- const errorBody = await safeResponseText(response);
1658
- throw new Error(`${this.providerKey} MCP ${resource} failed (${response.status}): ${errorBody}`);
1659
- }
1660
- const rpcPayload = await response.json();
1661
- const rpc = asRecord(rpcPayload);
1662
- const result = asRecord(rpc?.result) ?? {};
1663
- const structured = asRecord(result.structuredContent);
1664
- if (structured)
1665
- return structured;
1666
- const data = asRecord(result.data);
1667
- if (data)
1668
- return data;
1669
- 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;
1670
2036
  }
1671
- currentSource() {
2037
+ authorizationHeaders() {
2038
+ const token = this.accessToken ?? this.apiKey;
1672
2039
  return {
1673
- providerKey: this.providerKey,
1674
- transport: this.transport,
1675
- route: "primary"
2040
+ "Content-Type": "application/json",
2041
+ ...token ? { Authorization: `Bearer ${token}` } : {}
1676
2042
  };
1677
2043
  }
1678
- }
1679
- function safeJsonParse(raw) {
1680
- try {
1681
- return JSON.parse(raw);
1682
- } catch {
1683
- 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();
1684
2082
  }
1685
2083
  }
1686
2084
  function readHeader(headers, key) {
1687
- const match = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === key.toLowerCase());
1688
- if (!match)
2085
+ const target = key.toLowerCase();
2086
+ const entry = Object.entries(headers).find(([headerKey]) => headerKey.toLowerCase() === target);
2087
+ if (!entry)
1689
2088
  return;
1690
- const value = match[1];
2089
+ const value = entry[1];
1691
2090
  return Array.isArray(value) ? value[0] : value;
1692
2091
  }
1693
- function normalizeEntityType(value) {
1694
- if (!value)
1695
- return;
1696
- if (value === "activity" || value === "workout" || value === "sleep" || value === "biometric" || value === "nutrition") {
1697
- return value;
1698
- }
1699
- return;
1700
- }
1701
- function asRecord(value) {
1702
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1703
- 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
+ }
1704
2105
  }
1705
- return value;
1706
- }
1707
- function asArray2(value) {
1708
- return Array.isArray(value) ? value : undefined;
2106
+ return 0;
1709
2107
  }
1710
- function readString2(record, key) {
1711
- const value = record?.[key];
1712
- return typeof value === "string" ? value : undefined;
1713
- }
1714
- function readBoolean2(record, key) {
1715
- const value = record?.[key];
1716
- return typeof value === "boolean" ? value : undefined;
2108
+ function ensureTrailingSlash(value) {
2109
+ return value.endsWith("/") ? value : `${value}/`;
1717
2110
  }
1718
- function readNumber(record, key) {
1719
- const value = record?.[key];
1720
- 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
+ }
1721
2117
  }
1722
- async function safeResponseText(response) {
2118
+ async function safeReadText2(response) {
1723
2119
  try {
1724
2120
  return await response.text();
1725
2121
  } catch {
@@ -1727,133 +2123,530 @@ async function safeResponseText(response) {
1727
2123
  }
1728
2124
  }
1729
2125
 
1730
- // src/impls/health/providers.ts
1731
- function createProviderOptions(options, fallbackTransport) {
2126
+ // src/impls/health/official-health-providers.ts
2127
+ function buildSharedQuery(params) {
1732
2128
  return {
1733
- ...options,
1734
- 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
1735
2142
  };
1736
2143
  }
1737
2144
 
1738
2145
  class OpenWearablesHealthProvider extends BaseHealthProvider {
2146
+ upstreamProvider;
1739
2147
  constructor(options) {
1740
2148
  super({
1741
- providerKey: "health.openwearables",
1742
- ...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
1743
2153
  });
2154
+ this.upstreamProvider = options.upstreamProvider;
1744
2155
  }
1745
- }
1746
-
1747
- class WhoopHealthProvider extends BaseHealthProvider {
1748
- constructor(options) {
1749
- super({
1750
- providerKey: "health.whoop",
1751
- ...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")
1752
2162
  });
1753
2163
  }
1754
- }
1755
-
1756
- class AppleHealthBridgeProvider extends BaseHealthProvider {
1757
- constructor(options) {
1758
- super({
1759
- providerKey: "health.apple-health",
1760
- ...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))
1761
2170
  });
1762
2171
  }
1763
- }
1764
-
1765
- class OuraHealthProvider extends BaseHealthProvider {
1766
- constructor(options) {
1767
- super({
1768
- providerKey: "health.oura",
1769
- ...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))
1770
2178
  });
1771
2179
  }
1772
- }
1773
-
1774
- class StravaHealthProvider extends BaseHealthProvider {
1775
- constructor(options) {
1776
- super({
1777
- providerKey: "health.strava",
1778
- ...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))
1779
2186
  });
1780
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
+ }
1781
2215
  }
1782
2216
 
1783
- class GarminHealthProvider extends BaseHealthProvider {
2217
+ class AppleHealthBridgeProvider extends OpenWearablesHealthProvider {
1784
2218
  constructor(options) {
1785
2219
  super({
1786
- providerKey: "health.garmin",
1787
- ...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"
1788
2279
  });
1789
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
+ }
1790
2404
  }
1791
2405
 
1792
2406
  class FitbitHealthProvider extends BaseHealthProvider {
1793
2407
  constructor(options) {
1794
2408
  super({
1795
2409
  providerKey: "health.fitbit",
1796
- ...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")
1797
2449
  });
1798
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
+ }
1799
2472
  }
1800
2473
 
1801
- 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 {
1802
2494
  constructor(options) {
1803
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,
1804
2507
  providerKey: "health.myfitnesspal",
1805
- ...createProviderOptions(options, "official-api")
2508
+ upstreamProvider: "myfitnesspal"
1806
2509
  });
1807
2510
  }
1808
2511
  }
1809
2512
 
1810
- class EightSleepHealthProvider extends BaseHealthProvider {
2513
+ class EightSleepHealthProvider extends OpenWearablesHealthProvider {
1811
2514
  constructor(options) {
1812
2515
  super({
2516
+ ...options,
1813
2517
  providerKey: "health.eightsleep",
1814
- ...createProviderOptions(options, "official-api")
2518
+ upstreamProvider: "eightsleep"
1815
2519
  });
1816
2520
  }
1817
2521
  }
1818
2522
 
1819
- class PelotonHealthProvider extends BaseHealthProvider {
2523
+ class PelotonHealthProvider extends OpenWearablesHealthProvider {
1820
2524
  constructor(options) {
1821
2525
  super({
2526
+ ...options,
1822
2527
  providerKey: "health.peloton",
1823
- ...createProviderOptions(options, "official-api")
2528
+ upstreamProvider: "peloton"
1824
2529
  });
1825
2530
  }
1826
2531
  }
1827
2532
 
1828
2533
  class UnofficialHealthAutomationProvider extends BaseHealthProvider {
2534
+ providerSlugValue;
1829
2535
  constructor(options) {
1830
2536
  super({
1831
- ...createProviderOptions(options, "unofficial"),
1832
- providerKey: options.providerKey
2537
+ ...options,
2538
+ providerKey: options.providerKey,
2539
+ webhookSignatureHeader: "x-unofficial-signature"
1833
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
+ };
1834
2592
  }
1835
2593
  }
1836
-
1837
2594
  // src/impls/health-provider-factory.ts
1838
2595
  import {
1839
2596
  isUnofficialHealthProviderAllowed,
1840
2597
  resolveHealthStrategyOrder
1841
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
+ };
1842
2623
  function createHealthProviderFromContext(context, secrets) {
1843
2624
  const providerKey = context.spec.meta.key;
1844
2625
  const config = toFactoryConfig(context.config);
1845
2626
  const strategyOrder = buildStrategyOrder(config);
1846
- const errors = [];
1847
- for (const strategy of strategyOrder) {
1848
- 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);
1849
2642
  if (provider) {
1850
2643
  return provider;
1851
2644
  }
1852
- errors.push(`${strategy}: not available`);
2645
+ attemptLogs.push(`${strategy}: not available`);
1853
2646
  }
1854
- 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(", ")}.`);
1855
2648
  }
1856
- function createHealthProviderForStrategy(providerKey, strategy, config, secrets) {
2649
+ function createHealthProviderForStrategy(providerKey, strategy, route, config, secrets) {
1857
2650
  const options = {
1858
2651
  transport: strategy,
1859
2652
  apiBaseUrl: config.apiBaseUrl,
@@ -1861,10 +2654,21 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
1861
2654
  apiKey: getSecretString(secrets, "apiKey"),
1862
2655
  accessToken: getSecretString(secrets, "accessToken"),
1863
2656
  mcpAccessToken: getSecretString(secrets, "mcpAccessToken"),
1864
- 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
+ }
1865
2666
  };
1866
2667
  if (strategy === "aggregator-api" || strategy === "aggregator-mcp") {
1867
- return new OpenWearablesHealthProvider(options);
2668
+ return createAggregatorProvider(providerKey, {
2669
+ ...options,
2670
+ aggregatorKey: "health.openwearables"
2671
+ });
1868
2672
  }
1869
2673
  if (strategy === "unofficial") {
1870
2674
  if (!isUnofficialHealthProviderAllowed(providerKey, config)) {
@@ -1886,6 +2690,31 @@ function createHealthProviderForStrategy(providerKey, strategy, config, secrets)
1886
2690
  }
1887
2691
  return createOfficialProvider(providerKey, options);
1888
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
+ }
1889
2718
  function createOfficialProvider(providerKey, options) {
1890
2719
  switch (providerKey) {
1891
2720
  case "health.openwearables":
@@ -1903,11 +2732,20 @@ function createOfficialProvider(providerKey, options) {
1903
2732
  case "health.fitbit":
1904
2733
  return new FitbitHealthProvider(options);
1905
2734
  case "health.myfitnesspal":
1906
- return new MyFitnessPalHealthProvider(options);
2735
+ return new MyFitnessPalHealthProvider({
2736
+ ...options,
2737
+ transport: "aggregator-api"
2738
+ });
1907
2739
  case "health.eightsleep":
1908
- return new EightSleepHealthProvider(options);
2740
+ return new EightSleepHealthProvider({
2741
+ ...options,
2742
+ transport: "aggregator-api"
2743
+ });
1909
2744
  case "health.peloton":
1910
- return new PelotonHealthProvider(options);
2745
+ return new PelotonHealthProvider({
2746
+ ...options,
2747
+ transport: "aggregator-api"
2748
+ });
1911
2749
  default:
1912
2750
  throw new Error(`Unsupported health provider key: ${providerKey}`);
1913
2751
  }
@@ -1920,6 +2758,7 @@ function toFactoryConfig(config) {
1920
2758
  return {
1921
2759
  apiBaseUrl: asString(record.apiBaseUrl),
1922
2760
  mcpUrl: asString(record.mcpUrl),
2761
+ oauthTokenUrl: asString(record.oauthTokenUrl),
1923
2762
  defaultTransport: normalizeTransport(record.defaultTransport),
1924
2763
  strategyOrder: normalizeTransportArray(record.strategyOrder),
1925
2764
  allowUnofficial: typeof record.allowUnofficial === "boolean" ? record.allowUnofficial : false,
@@ -1948,6 +2787,27 @@ function normalizeTransportArray(value) {
1948
2787
  const transports = value.map((item) => normalizeTransport(item)).filter((item) => Boolean(item));
1949
2788
  return transports.length > 0 ? transports : undefined;
1950
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
+ }
1951
2811
  function getSecretString(secrets, key) {
1952
2812
  const value = secrets[key];
1953
2813
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
@@ -2223,45 +3083,474 @@ function mapFinishReason(reason) {
2223
3083
  }
2224
3084
  }
2225
3085
 
2226
- // src/impls/mistral-embedding.ts
2227
- 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";
2228
3454
 
2229
- class MistralEmbeddingProvider {
2230
- client;
3455
+ class MistralConversationalProvider {
3456
+ apiKey;
2231
3457
  defaultModel;
3458
+ defaultVoiceId;
3459
+ baseUrl;
3460
+ fetchImpl;
3461
+ sttProvider;
2232
3462
  constructor(options) {
2233
3463
  if (!options.apiKey) {
2234
- throw new Error("MistralEmbeddingProvider requires an apiKey");
3464
+ throw new Error("MistralConversationalProvider requires an apiKey");
2235
3465
  }
2236
- 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({
2237
3472
  apiKey: options.apiKey,
2238
- serverURL: options.serverURL
3473
+ defaultModel: options.sttOptions?.defaultModel,
3474
+ defaultLanguage: options.sttOptions?.defaultLanguage,
3475
+ serverURL: options.sttOptions?.serverURL ?? options.serverURL,
3476
+ fetchImpl: this.fetchImpl
2239
3477
  });
2240
- this.defaultModel = options.defaultModel ?? "mistral-embed";
2241
3478
  }
2242
- async embedDocuments(documents, options) {
2243
- if (documents.length === 0)
2244
- return [];
2245
- const model = options?.model ?? this.defaultModel;
2246
- const response = await this.client.embeddings.create({
2247
- model,
2248
- 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
2249
3488
  });
2250
- return response.data.map((item, index) => ({
2251
- id: documents[index]?.id ?? (item.index != null ? `embedding-${item.index}` : `embedding-${index}`),
2252
- vector: item.embedding ?? [],
2253
- dimensions: item.embedding?.length ?? 0,
2254
- model: response.model,
2255
- metadata: documents[index]?.metadata ? Object.fromEntries(Object.entries(documents[index]?.metadata ?? {}).map(([key, value]) => [key, String(value)])) : undefined
2256
- }));
2257
3489
  }
2258
- async embedQuery(query, options) {
2259
- const [result] = await this.embedDocuments([{ id: "query", text: query }], options);
2260
- if (!result) {
2261
- 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 });
2262
3505
  }
2263
- 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("");
2264
3546
  }
3547
+ return "";
3548
+ }
3549
+ function asRecord3(value) {
3550
+ if (value && typeof value === "object") {
3551
+ return value;
3552
+ }
3553
+ return {};
2265
3554
  }
2266
3555
 
2267
3556
  // src/impls/qdrant-vector.ts
@@ -2663,7 +3952,7 @@ function distanceToScore(distance, metric) {
2663
3952
 
2664
3953
  // src/impls/stripe-payments.ts
2665
3954
  import Stripe from "stripe";
2666
- var API_VERSION = "2026-01-28.clover";
3955
+ var API_VERSION = "2026-02-25.clover";
2667
3956
 
2668
3957
  class StripePaymentsProvider {
2669
3958
  stripe;
@@ -3328,6 +4617,318 @@ function mapStatus(status) {
3328
4617
  }
3329
4618
  }
3330
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 "node: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
+
3331
4932
  // src/impls/powens-client.ts
3332
4933
  import { URL as URL2 } from "node:url";
3333
4934
  var POWENS_BASE_URL = {
@@ -3834,7 +5435,7 @@ function resolveLabelIds(defaults, tags) {
3834
5435
  }
3835
5436
 
3836
5437
  // src/impls/jira.ts
3837
- import { Buffer as Buffer4 } from "node:buffer";
5438
+ import { Buffer as Buffer5 } from "node:buffer";
3838
5439
 
3839
5440
  class JiraProjectManagementProvider {
3840
5441
  siteUrl;
@@ -3911,7 +5512,7 @@ function normalizeSiteUrl(siteUrl) {
3911
5512
  return siteUrl.replace(/\/$/, "");
3912
5513
  }
3913
5514
  function buildAuthHeader(email, apiToken) {
3914
- const token = Buffer4.from(`${email}:${apiToken}`).toString("base64");
5515
+ const token = Buffer5.from(`${email}:${apiToken}`).toString("base64");
3915
5516
  return `Basic ${token}`;
3916
5517
  }
3917
5518
  function resolveIssueType(type, defaults) {
@@ -4114,7 +5715,7 @@ function buildParagraphBlocks(text) {
4114
5715
  }
4115
5716
 
4116
5717
  // src/impls/tldv-meeting-recorder.ts
4117
- var DEFAULT_BASE_URL4 = "https://pasta.tldv.io/v1alpha1";
5718
+ var DEFAULT_BASE_URL6 = "https://pasta.tldv.io/v1alpha1";
4118
5719
 
4119
5720
  class TldvMeetingRecorderProvider {
4120
5721
  apiKey;
@@ -4122,7 +5723,7 @@ class TldvMeetingRecorderProvider {
4122
5723
  defaultPageSize;
4123
5724
  constructor(options) {
4124
5725
  this.apiKey = options.apiKey;
4125
- this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL4;
5726
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL6;
4126
5727
  this.defaultPageSize = options.pageSize;
4127
5728
  }
4128
5729
  async listMeetings(params) {
@@ -4257,10 +5858,30 @@ async function safeReadError4(response) {
4257
5858
  }
4258
5859
 
4259
5860
  // src/impls/provider-factory.ts
4260
- import { Buffer as Buffer5 } from "node:buffer";
5861
+ import { Buffer as Buffer6 } from "node: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";
4261
5865
  var SECRET_CACHE = new Map;
4262
5866
 
4263
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
+ }
4264
5885
  async createPaymentsProvider(context) {
4265
5886
  const secrets = await this.loadSecrets(context);
4266
5887
  switch (context.spec.meta.key) {
@@ -4269,6 +5890,9 @@ class IntegrationProviderFactory {
4269
5890
  apiKey: requireSecret(secrets, "apiKey", "Stripe API key is required")
4270
5891
  });
4271
5892
  default:
5893
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
5894
+ return this.composioFallback.createPaymentsProxy(context);
5895
+ }
4272
5896
  throw new Error(`Unsupported payments integration: ${context.spec.meta.key}`);
4273
5897
  }
4274
5898
  }
@@ -4282,6 +5906,9 @@ class IntegrationProviderFactory {
4282
5906
  messageStream: context.config.messageStream
4283
5907
  });
4284
5908
  default:
5909
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
5910
+ return this.composioFallback.createEmailProxy(context);
5911
+ }
4285
5912
  throw new Error(`Unsupported email integration: ${context.spec.meta.key}`);
4286
5913
  }
4287
5914
  }
@@ -4295,9 +5922,48 @@ class IntegrationProviderFactory {
4295
5922
  fromNumber: context.config.fromNumber
4296
5923
  });
4297
5924
  default:
5925
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
5926
+ return this.composioFallback.createMessagingProxy(context);
5927
+ }
4298
5928
  throw new Error(`Unsupported SMS integration: ${context.spec.meta.key}`);
4299
5929
  }
4300
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
+ }
4301
5967
  async createVectorStoreProvider(context) {
4302
5968
  const secrets = await this.loadSecrets(context);
4303
5969
  const config = context.config;
@@ -4318,6 +5984,9 @@ class IntegrationProviderFactory {
4318
5984
  sslMode: config?.sslMode
4319
5985
  });
4320
5986
  default:
5987
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
5988
+ return this.composioFallback.createGenericProxy(context);
5989
+ }
4321
5990
  throw new Error(`Unsupported vector store integration: ${context.spec.meta.key}`);
4322
5991
  }
4323
5992
  }
@@ -4334,6 +6003,9 @@ class IntegrationProviderFactory {
4334
6003
  personalApiKey: requireSecret(secrets, "personalApiKey", "PostHog personalApiKey is required")
4335
6004
  });
4336
6005
  default:
6006
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6007
+ return this.composioFallback.createGenericProxy(context);
6008
+ }
4337
6009
  throw new Error(`Unsupported analytics integration: ${context.spec.meta.key}`);
4338
6010
  }
4339
6011
  }
@@ -4348,6 +6020,9 @@ class IntegrationProviderFactory {
4348
6020
  sslMode: config?.sslMode
4349
6021
  });
4350
6022
  default:
6023
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6024
+ return this.composioFallback.createGenericProxy(context);
6025
+ }
4351
6026
  throw new Error(`Unsupported database integration: ${context.spec.meta.key}`);
4352
6027
  }
4353
6028
  }
@@ -4361,6 +6036,9 @@ class IntegrationProviderFactory {
4361
6036
  clientOptions: secrets.type === "service_account" ? { credentials: secrets } : undefined
4362
6037
  });
4363
6038
  default:
6039
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6040
+ return this.composioFallback.createGenericProxy(context);
6041
+ }
4364
6042
  throw new Error(`Unsupported storage integration: ${context.spec.meta.key}`);
4365
6043
  }
4366
6044
  }
@@ -4393,9 +6071,53 @@ class IntegrationProviderFactory {
4393
6071
  pollIntervalMs: config?.pollIntervalMs
4394
6072
  });
4395
6073
  default:
6074
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6075
+ return this.composioFallback.createGenericProxy(context);
6076
+ }
4396
6077
  throw new Error(`Unsupported voice integration: ${context.spec.meta.key}`);
4397
6078
  }
4398
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
+ }
4399
6121
  async createProjectManagementProvider(context) {
4400
6122
  const secrets = await this.loadSecrets(context);
4401
6123
  const config = context.config;
@@ -4433,6 +6155,9 @@ class IntegrationProviderFactory {
4433
6155
  descriptionProperty: config?.descriptionProperty
4434
6156
  });
4435
6157
  default:
6158
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6159
+ return this.composioFallback.createProjectManagementProxy(context);
6160
+ }
4436
6161
  throw new Error(`Unsupported project management integration: ${context.spec.meta.key}`);
4437
6162
  }
4438
6163
  }
@@ -4481,6 +6206,9 @@ class IntegrationProviderFactory {
4481
6206
  webhookSecret: secrets.webhookSecret
4482
6207
  });
4483
6208
  default:
6209
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6210
+ return this.composioFallback.createGenericProxy(context);
6211
+ }
4484
6212
  throw new Error(`Unsupported meeting recorder integration: ${context.spec.meta.key}`);
4485
6213
  }
4486
6214
  }
@@ -4493,6 +6221,9 @@ class IntegrationProviderFactory {
4493
6221
  defaultModel: context.config.model
4494
6222
  });
4495
6223
  default:
6224
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6225
+ return this.composioFallback.createGenericProxy(context);
6226
+ }
4496
6227
  throw new Error(`Unsupported LLM integration: ${context.spec.meta.key}`);
4497
6228
  }
4498
6229
  }
@@ -4505,6 +6236,9 @@ class IntegrationProviderFactory {
4505
6236
  defaultModel: context.config.embeddingModel
4506
6237
  });
4507
6238
  default:
6239
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6240
+ return this.composioFallback.createGenericProxy(context);
6241
+ }
4508
6242
  throw new Error(`Unsupported embeddings integration: ${context.spec.meta.key}`);
4509
6243
  }
4510
6244
  }
@@ -4526,6 +6260,9 @@ class IntegrationProviderFactory {
4526
6260
  });
4527
6261
  }
4528
6262
  default:
6263
+ if (this.composioFallback?.canHandle(context.spec.meta.key)) {
6264
+ return this.composioFallback.createGenericProxy(context);
6265
+ }
4529
6266
  throw new Error(`Unsupported open banking integration: ${context.spec.meta.key}`);
4530
6267
  }
4531
6268
  }
@@ -4546,7 +6283,7 @@ class IntegrationProviderFactory {
4546
6283
  }
4547
6284
  }
4548
6285
  function parseSecret(secret) {
4549
- const text = Buffer5.from(secret.data).toString("utf-8").trim();
6286
+ const text = Buffer6.from(secret.data).toString("utf-8").trim();
4550
6287
  if (!text)
4551
6288
  return {};
4552
6289
  try {