@jini-ai/integrations 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +51 -0
  3. package/dist/composio/bounded-data.d.ts +36 -0
  4. package/dist/composio/bounded-data.d.ts.map +1 -0
  5. package/dist/composio/bounded-data.js +181 -0
  6. package/dist/composio/bounded-data.js.map +1 -0
  7. package/dist/composio/catalog.d.ts +147 -0
  8. package/dist/composio/catalog.d.ts.map +1 -0
  9. package/dist/composio/catalog.js +126 -0
  10. package/dist/composio/catalog.js.map +1 -0
  11. package/dist/composio/composio-config.d.ts +42 -0
  12. package/dist/composio/composio-config.d.ts.map +1 -0
  13. package/dist/composio/composio-config.js +145 -0
  14. package/dist/composio/composio-config.js.map +1 -0
  15. package/dist/composio/composio-descriptions.d.ts +16 -0
  16. package/dist/composio/composio-descriptions.d.ts.map +1 -0
  17. package/dist/composio/composio-descriptions.js +766 -0
  18. package/dist/composio/composio-descriptions.js.map +1 -0
  19. package/dist/composio/composio.d.ts +364 -0
  20. package/dist/composio/composio.d.ts.map +1 -0
  21. package/dist/composio/composio.js +1902 -0
  22. package/dist/composio/composio.js.map +1 -0
  23. package/dist/composio/errors.d.ts +15 -0
  24. package/dist/composio/errors.d.ts.map +1 -0
  25. package/dist/composio/errors.js +16 -0
  26. package/dist/composio/errors.js.map +1 -0
  27. package/dist/composio/file-lock.d.ts +13 -0
  28. package/dist/composio/file-lock.d.ts.map +1 -0
  29. package/dist/composio/file-lock.js +92 -0
  30. package/dist/composio/file-lock.js.map +1 -0
  31. package/dist/composio/index.d.ts +18 -0
  32. package/dist/composio/index.d.ts.map +1 -0
  33. package/dist/composio/index.js +7 -0
  34. package/dist/composio/index.js.map +1 -0
  35. package/dist/composio/json-schema.d.ts +93 -0
  36. package/dist/composio/json-schema.d.ts.map +1 -0
  37. package/dist/composio/json-schema.js +498 -0
  38. package/dist/composio/json-schema.js.map +1 -0
  39. package/dist/composio/output-protection.d.ts +32 -0
  40. package/dist/composio/output-protection.d.ts.map +1 -0
  41. package/dist/composio/output-protection.js +105 -0
  42. package/dist/composio/output-protection.js.map +1 -0
  43. package/dist/composio/service.d.ts +181 -0
  44. package/dist/composio/service.d.ts.map +1 -0
  45. package/dist/composio/service.js +758 -0
  46. package/dist/composio/service.js.map +1 -0
  47. package/dist/media-providers/capability-registry.d.ts +28 -0
  48. package/dist/media-providers/capability-registry.d.ts.map +1 -0
  49. package/dist/media-providers/capability-registry.js +27 -0
  50. package/dist/media-providers/capability-registry.js.map +1 -0
  51. package/dist/media-providers/catalog.d.ts +20 -0
  52. package/dist/media-providers/catalog.d.ts.map +1 -0
  53. package/dist/media-providers/catalog.js +20 -0
  54. package/dist/media-providers/catalog.js.map +1 -0
  55. package/dist/media-providers/dispatch/context.d.ts +21 -0
  56. package/dist/media-providers/dispatch/context.d.ts.map +1 -0
  57. package/dist/media-providers/dispatch/context.js +36 -0
  58. package/dist/media-providers/dispatch/context.js.map +1 -0
  59. package/dist/media-providers/dispatch/credentials.d.ts +4 -0
  60. package/dist/media-providers/dispatch/credentials.d.ts.map +1 -0
  61. package/dist/media-providers/dispatch/credentials.js +25 -0
  62. package/dist/media-providers/dispatch/credentials.js.map +1 -0
  63. package/dist/media-providers/dispatch/engine.d.ts +30 -0
  64. package/dist/media-providers/dispatch/engine.d.ts.map +1 -0
  65. package/dist/media-providers/dispatch/engine.js +208 -0
  66. package/dist/media-providers/dispatch/engine.js.map +1 -0
  67. package/dist/media-providers/dispatch/index.d.ts +25 -0
  68. package/dist/media-providers/dispatch/index.d.ts.map +1 -0
  69. package/dist/media-providers/dispatch/index.js +24 -0
  70. package/dist/media-providers/dispatch/index.js.map +1 -0
  71. package/dist/media-providers/dispatch/openai-compatible.d.ts +44 -0
  72. package/dist/media-providers/dispatch/openai-compatible.d.ts.map +1 -0
  73. package/dist/media-providers/dispatch/openai-compatible.js +214 -0
  74. package/dist/media-providers/dispatch/openai-compatible.js.map +1 -0
  75. package/dist/media-providers/dispatch/providers/aihubmix-shared.d.ts +72 -0
  76. package/dist/media-providers/dispatch/providers/aihubmix-shared.d.ts.map +1 -0
  77. package/dist/media-providers/dispatch/providers/aihubmix-shared.js +131 -0
  78. package/dist/media-providers/dispatch/providers/aihubmix-shared.js.map +1 -0
  79. package/dist/media-providers/dispatch/providers/aihubmix.d.ts +4 -0
  80. package/dist/media-providers/dispatch/providers/aihubmix.d.ts.map +1 -0
  81. package/dist/media-providers/dispatch/providers/aihubmix.js +205 -0
  82. package/dist/media-providers/dispatch/providers/aihubmix.js.map +1 -0
  83. package/dist/media-providers/dispatch/providers/custom-image.d.ts +6 -0
  84. package/dist/media-providers/dispatch/providers/custom-image.d.ts.map +1 -0
  85. package/dist/media-providers/dispatch/providers/custom-image.js +94 -0
  86. package/dist/media-providers/dispatch/providers/custom-image.js.map +1 -0
  87. package/dist/media-providers/dispatch/providers/elevenlabs.d.ts +4 -0
  88. package/dist/media-providers/dispatch/providers/elevenlabs.d.ts.map +1 -0
  89. package/dist/media-providers/dispatch/providers/elevenlabs.js +163 -0
  90. package/dist/media-providers/dispatch/providers/elevenlabs.js.map +1 -0
  91. package/dist/media-providers/dispatch/providers/fishaudio.d.ts +3 -0
  92. package/dist/media-providers/dispatch/providers/fishaudio.d.ts.map +1 -0
  93. package/dist/media-providers/dispatch/providers/fishaudio.js +83 -0
  94. package/dist/media-providers/dispatch/providers/fishaudio.js.map +1 -0
  95. package/dist/media-providers/dispatch/providers/grok.d.ts +15 -0
  96. package/dist/media-providers/dispatch/providers/grok.d.ts.map +1 -0
  97. package/dist/media-providers/dispatch/providers/grok.js +150 -0
  98. package/dist/media-providers/dispatch/providers/grok.js.map +1 -0
  99. package/dist/media-providers/dispatch/providers/imagerouter.d.ts +6 -0
  100. package/dist/media-providers/dispatch/providers/imagerouter.d.ts.map +1 -0
  101. package/dist/media-providers/dispatch/providers/imagerouter.js +131 -0
  102. package/dist/media-providers/dispatch/providers/imagerouter.js.map +1 -0
  103. package/dist/media-providers/dispatch/providers/minimax.d.ts +3 -0
  104. package/dist/media-providers/dispatch/providers/minimax.d.ts.map +1 -0
  105. package/dist/media-providers/dispatch/providers/minimax.js +96 -0
  106. package/dist/media-providers/dispatch/providers/minimax.js.map +1 -0
  107. package/dist/media-providers/dispatch/providers/nanobanana.d.ts +3 -0
  108. package/dist/media-providers/dispatch/providers/nanobanana.d.ts.map +1 -0
  109. package/dist/media-providers/dispatch/providers/nanobanana.js +142 -0
  110. package/dist/media-providers/dispatch/providers/nanobanana.js.map +1 -0
  111. package/dist/media-providers/dispatch/providers/openai.d.ts +4 -0
  112. package/dist/media-providers/dispatch/providers/openai.d.ts.map +1 -0
  113. package/dist/media-providers/dispatch/providers/openai.js +231 -0
  114. package/dist/media-providers/dispatch/providers/openai.js.map +1 -0
  115. package/dist/media-providers/dispatch/providers/openrouter.d.ts +13 -0
  116. package/dist/media-providers/dispatch/providers/openrouter.d.ts.map +1 -0
  117. package/dist/media-providers/dispatch/providers/openrouter.js +152 -0
  118. package/dist/media-providers/dispatch/providers/openrouter.js.map +1 -0
  119. package/dist/media-providers/dispatch/providers/senseaudio.d.ts +4 -0
  120. package/dist/media-providers/dispatch/providers/senseaudio.d.ts.map +1 -0
  121. package/dist/media-providers/dispatch/providers/senseaudio.js +190 -0
  122. package/dist/media-providers/dispatch/providers/senseaudio.js.map +1 -0
  123. package/dist/media-providers/dispatch/providers/volcengine.d.ts +3 -0
  124. package/dist/media-providers/dispatch/providers/volcengine.d.ts.map +1 -0
  125. package/dist/media-providers/dispatch/providers/volcengine.js +95 -0
  126. package/dist/media-providers/dispatch/providers/volcengine.js.map +1 -0
  127. package/dist/media-providers/dispatch/response-parsers.d.ts +44 -0
  128. package/dist/media-providers/dispatch/response-parsers.d.ts.map +1 -0
  129. package/dist/media-providers/dispatch/response-parsers.js +103 -0
  130. package/dist/media-providers/dispatch/response-parsers.js.map +1 -0
  131. package/dist/media-providers/dispatch/ssrf-guard.d.ts +51 -0
  132. package/dist/media-providers/dispatch/ssrf-guard.d.ts.map +1 -0
  133. package/dist/media-providers/dispatch/ssrf-guard.js +215 -0
  134. package/dist/media-providers/dispatch/ssrf-guard.js.map +1 -0
  135. package/dist/media-providers/dispatch/stub.d.ts +5 -0
  136. package/dist/media-providers/dispatch/stub.d.ts.map +1 -0
  137. package/dist/media-providers/dispatch/stub.js +98 -0
  138. package/dist/media-providers/dispatch/stub.js.map +1 -0
  139. package/dist/media-providers/dispatch/types.d.ts +133 -0
  140. package/dist/media-providers/dispatch/types.d.ts.map +1 -0
  141. package/dist/media-providers/dispatch/types.js +2 -0
  142. package/dist/media-providers/dispatch/types.js.map +1 -0
  143. package/dist/media-providers/dispatch/vendor-adapter.d.ts +54 -0
  144. package/dist/media-providers/dispatch/vendor-adapter.d.ts.map +1 -0
  145. package/dist/media-providers/dispatch/vendor-adapter.js +68 -0
  146. package/dist/media-providers/dispatch/vendor-adapter.js.map +1 -0
  147. package/dist/media-providers/dispatch/vendor-registry.d.ts +36 -0
  148. package/dist/media-providers/dispatch/vendor-registry.d.ts.map +1 -0
  149. package/dist/media-providers/dispatch/vendor-registry.js +46 -0
  150. package/dist/media-providers/dispatch/vendor-registry.js.map +1 -0
  151. package/dist/media-providers/index.d.ts +22 -0
  152. package/dist/media-providers/index.d.ts.map +1 -0
  153. package/dist/media-providers/index.js +22 -0
  154. package/dist/media-providers/index.js.map +1 -0
  155. package/dist/media-providers/policy.d.ts +49 -0
  156. package/dist/media-providers/policy.d.ts.map +1 -0
  157. package/dist/media-providers/policy.js +39 -0
  158. package/dist/media-providers/policy.js.map +1 -0
  159. package/dist/media-providers/providers.d.ts +33 -0
  160. package/dist/media-providers/providers.d.ts.map +1 -0
  161. package/dist/media-providers/providers.js +200 -0
  162. package/dist/media-providers/providers.js.map +1 -0
  163. package/dist/media-providers/seed.d.ts +14 -0
  164. package/dist/media-providers/seed.d.ts.map +1 -0
  165. package/dist/media-providers/seed.js +109 -0
  166. package/dist/media-providers/seed.js.map +1 -0
  167. package/dist/media-providers/sqlite-task-store.d.ts +29 -0
  168. package/dist/media-providers/sqlite-task-store.d.ts.map +1 -0
  169. package/dist/media-providers/sqlite-task-store.js +234 -0
  170. package/dist/media-providers/sqlite-task-store.js.map +1 -0
  171. package/dist/media-providers/staging.d.ts +29 -0
  172. package/dist/media-providers/staging.d.ts.map +1 -0
  173. package/dist/media-providers/staging.js +141 -0
  174. package/dist/media-providers/staging.js.map +1 -0
  175. package/dist/media-providers/task-store.d.ts +94 -0
  176. package/dist/media-providers/task-store.d.ts.map +1 -0
  177. package/dist/media-providers/task-store.js +160 -0
  178. package/dist/media-providers/task-store.js.map +1 -0
  179. package/dist/media-providers/tokens.d.ts +7 -0
  180. package/dist/media-providers/tokens.d.ts.map +1 -0
  181. package/dist/media-providers/tokens.js +13 -0
  182. package/dist/media-providers/tokens.js.map +1 -0
  183. package/dist/media-providers/types.d.ts +123 -0
  184. package/dist/media-providers/types.d.ts.map +1 -0
  185. package/dist/media-providers/types.js +12 -0
  186. package/dist/media-providers/types.js.map +1 -0
  187. package/dist/media-providers/video-request.d.ts +62 -0
  188. package/dist/media-providers/video-request.d.ts.map +1 -0
  189. package/dist/media-providers/video-request.js +219 -0
  190. package/dist/media-providers/video-request.js.map +1 -0
  191. package/package.json +93 -0
@@ -0,0 +1,1902 @@
1
+ /**
2
+ * @module composio
3
+ *
4
+ * Injected Composio catalog, OAuth, connected-account, and execution adapter.
5
+ */
6
+ import crypto from 'node:crypto';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { BoundedDataError, COMPOSIO_MAX_CACHE_BYTES, COMPOSIO_MAX_ERROR_RESPONSE_BYTES, COMPOSIO_MAX_RESPONSE_BYTES, readBoundedResponseJson, readBoundedUtf8File, toStructurallyBoundedJsonValue, } from './bounded-data.js';
10
+ import { classifyConnectorToolSafety, defineConnectorTool, } from './catalog.js';
11
+ import { getComposioToolkitMetadata } from './composio-descriptions.js';
12
+ import { ConnectorServiceError } from './errors.js';
13
+ import { assertConnectorInputMatchesSchema, getConnectorSchemaSupportError, } from './json-schema.js';
14
+ import { withExclusiveFileLock } from './file-lock.js';
15
+ import { protectConnectorOutput } from './output-protection.js';
16
+ const DEFAULT_COMPOSIO_BASE_URL = 'https://backend.composio.dev';
17
+ const DEFAULT_COMPOSIO_TIMEOUT_MS = 30_000;
18
+ const OAUTH_STATE_TTL_MS = 10 * 60 * 1000;
19
+ const DISCOVERY_CACHE_TTL_MS = 60_000;
20
+ const PERSISTED_CATALOG_REFRESH_MS = 24 * 60 * 60 * 1000;
21
+ const DEFAULT_USER_AGENT = '@jini-ai/integrations/0.1';
22
+ const COMPOSIO_MAX_LIST_ITEMS = 1_000;
23
+ const COMPOSIO_READ_ONLY_TOOL_SAFETY_OVERRIDES = new Set([
24
+ 'notion:notion_search_notion_page',
25
+ ]);
26
+ // Exact package-owned authority for provider tools that are intentionally
27
+ // available even though their full schema is hydrated from Composio.
28
+ const COMPOSIO_STATIC_DISCOVERED_TOOL_ALLOWLIST = new Set([
29
+ 'github:github_list_repositories',
30
+ ]);
31
+ const COMPOSIO_READ_ONLY_TOOL_SAFETY = {
32
+ sideEffect: 'read',
33
+ approval: 'auto',
34
+ reason: 'Provider-specific override: this Composio tool is a read-only search/list operation.',
35
+ };
36
+ function deepFreeze(value) {
37
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
38
+ for (const child of Object.values(value))
39
+ deepFreeze(child);
40
+ Object.freeze(value);
41
+ }
42
+ return value;
43
+ }
44
+ /** Default featured toolkit definitions shipped by the vendor adapter. */
45
+ export const FEATURED_COMPOSIO_CATALOG = deepFreeze([
46
+ {
47
+ id: 'github',
48
+ name: 'GitHub',
49
+ provider: 'composio',
50
+ category: 'Developer',
51
+ description: 'Search and inspect GitHub repositories, issues, and pull requests.',
52
+ providerConnectorId: 'GITHUB',
53
+ authentication: 'composio',
54
+ tools: [
55
+ defineConnectorTool({
56
+ name: 'github.github_search_repositories',
57
+ providerToolId: 'GITHUB_SEARCH_REPOSITORIES',
58
+ title: 'Search repositories',
59
+ description: 'Search public and private repositories.',
60
+ inputSchemaJson: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'], additionalProperties: false },
61
+ outputSchemaJson: { type: 'object', additionalProperties: true },
62
+ requiredScopes: ['read'],
63
+ }),
64
+ defineConnectorTool({
65
+ name: 'github.github_get_issue',
66
+ providerToolId: 'GITHUB_GET_ISSUE',
67
+ title: 'Get issue',
68
+ description: 'Read a GitHub issue by owner, repository, and issue number.',
69
+ inputSchemaJson: { type: 'object', properties: { owner: { type: 'string' }, repo: { type: 'string' }, issue_number: { type: 'number' } }, required: ['owner', 'repo', 'issue_number'], additionalProperties: false },
70
+ outputSchemaJson: { type: 'object', additionalProperties: true },
71
+ requiredScopes: ['issues:read'],
72
+ }),
73
+ ],
74
+ allowedToolNames: ['github.github_search_repositories', 'github.github_get_issue'],
75
+ featuredToolNames: ['github.github_search_repositories', 'github.github_get_issue'],
76
+ minimumApproval: 'auto',
77
+ toolCount: 2,
78
+ },
79
+ {
80
+ id: 'notion',
81
+ name: 'Notion',
82
+ provider: 'composio',
83
+ category: 'Productivity',
84
+ description: 'Search and read Notion pages and databases.',
85
+ providerConnectorId: 'NOTION',
86
+ authentication: 'composio',
87
+ tools: [
88
+ defineConnectorTool({
89
+ name: 'notion.notion_search',
90
+ providerToolId: 'NOTION_SEARCH',
91
+ title: 'Search Notion',
92
+ description: 'Search Notion pages and databases.',
93
+ inputSchemaJson: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'], additionalProperties: false },
94
+ outputSchemaJson: { type: 'object', additionalProperties: true },
95
+ requiredScopes: ['read'],
96
+ }),
97
+ defineConnectorTool({
98
+ name: 'notion.notion_fetch_database',
99
+ providerToolId: 'NOTION_FETCH_DATABASE',
100
+ title: 'Fetch database',
101
+ description: 'Read a Notion database by id.',
102
+ inputSchemaJson: { type: 'object', properties: { database_id: { type: 'string' } }, required: ['database_id'], additionalProperties: false },
103
+ outputSchemaJson: { type: 'object', additionalProperties: true },
104
+ requiredScopes: ['databases:read'],
105
+ }),
106
+ ],
107
+ allowedToolNames: ['notion.notion_search', 'notion.notion_fetch_database'],
108
+ featuredToolNames: ['notion.notion_search', 'notion.notion_fetch_database'],
109
+ minimumApproval: 'auto',
110
+ toolCount: 48,
111
+ },
112
+ {
113
+ id: 'google_drive',
114
+ name: 'Google Drive',
115
+ provider: 'composio',
116
+ category: 'Storage',
117
+ description: 'Search and read files from Google Drive.',
118
+ providerConnectorId: 'GOOGLEDRIVE',
119
+ authentication: 'composio',
120
+ tools: [
121
+ defineConnectorTool({
122
+ name: 'google_drive.googledrive_search',
123
+ providerToolId: 'GOOGLEDRIVE_SEARCH',
124
+ title: 'Search Drive',
125
+ description: 'Search files in Google Drive.',
126
+ inputSchemaJson: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'], additionalProperties: false },
127
+ outputSchemaJson: { type: 'object', additionalProperties: true },
128
+ requiredScopes: ['drive.readonly'],
129
+ }),
130
+ defineConnectorTool({
131
+ name: 'google_drive.googledrive_get_file',
132
+ providerToolId: 'GOOGLEDRIVE_GET_FILE',
133
+ title: 'Get file',
134
+ description: 'Read Google Drive file metadata by id.',
135
+ inputSchemaJson: { type: 'object', properties: { file_id: { type: 'string' } }, required: ['file_id'], additionalProperties: false },
136
+ outputSchemaJson: { type: 'object', additionalProperties: true },
137
+ requiredScopes: ['drive.readonly'],
138
+ }),
139
+ ],
140
+ allowedToolNames: ['google_drive.googledrive_search', 'google_drive.googledrive_get_file'],
141
+ featuredToolNames: ['google_drive.googledrive_search', 'google_drive.googledrive_get_file'],
142
+ minimumApproval: 'auto',
143
+ toolCount: 2,
144
+ },
145
+ ]);
146
+ /**
147
+ * Point-in-time toolkit inventory last reviewed
148
+ * against Composio's public toolkit API on 2026-07-23.
149
+ */
150
+ export const DOCUMENTED_COMPOSIO_TOOLKITS = deepFreeze([
151
+ { name: 'Airtable', slug: 'AIRTABLE' },
152
+ { name: 'Apaleo', slug: 'APALEO' },
153
+ { name: 'Asana', slug: 'ASANA' },
154
+ { name: 'Attio', slug: 'ATTIO' },
155
+ { name: 'Basecamp', slug: 'BASECAMP' },
156
+ { name: 'Bitbucket', slug: 'BITBUCKET' },
157
+ { name: 'Blackbaud', slug: 'BLACKBAUD' },
158
+ { name: 'Boldsign', slug: 'BOLDSIGN' },
159
+ { name: 'Box', slug: 'BOX' },
160
+ { name: 'Cal', slug: 'CAL' },
161
+ { name: 'Calendly', slug: 'CALENDLY' },
162
+ { name: 'Canva', slug: 'CANVA' },
163
+ { name: 'Capsule CRM', slug: 'CAPSULE_CRM' },
164
+ { name: 'ClickUp', slug: 'CLICKUP' },
165
+ { name: 'Confluence', slug: 'CONFLUENCE' },
166
+ { name: 'Contentful', slug: 'CONTENTFUL' },
167
+ { name: 'Convex', slug: 'CONVEX' },
168
+ { name: 'Crowdin', slug: 'CROWDIN' },
169
+ { name: 'Dart', slug: 'DART' },
170
+ { name: 'Dialpad', slug: 'DIALPAD' },
171
+ { name: 'DigitalOcean', slug: 'DIGITAL_OCEAN' },
172
+ { name: 'Discord', slug: 'DISCORD' },
173
+ { name: 'Discord Bot', slug: 'DISCORDBOT' },
174
+ { name: 'Dropbox', slug: 'DROPBOX' },
175
+ { name: 'Dub', slug: 'DUB' },
176
+ { name: 'Dynamics 365', slug: 'DYNAMICS365' },
177
+ { name: 'Eventbrite', slug: 'EVENTBRITE' },
178
+ { name: 'Excel', slug: 'EXCEL' },
179
+ { name: 'Exist', slug: 'EXIST' },
180
+ { name: 'Facebook', slug: 'FACEBOOK' },
181
+ { name: 'Fathom', slug: 'FATHOM' },
182
+ { name: 'Figma', slug: 'FIGMA' },
183
+ { name: 'Freeagent', slug: 'FREEAGENT' },
184
+ { name: 'FreshBooks', slug: 'FRESHBOOKS' },
185
+ { name: 'GitHub', slug: 'GITHUB' },
186
+ { name: 'GitLab', slug: 'GITLAB' },
187
+ { name: 'Gmail', slug: 'GMAIL' },
188
+ { name: 'Google Ads', slug: 'GOOGLEADS' },
189
+ { name: 'Google Analytics', slug: 'GOOGLE_ANALYTICS' },
190
+ { name: 'Google BigQuery', slug: 'GOOGLEBIGQUERY' },
191
+ { name: 'Google Calendar', slug: 'GOOGLECALENDAR' },
192
+ { name: 'Google Classroom', slug: 'GOOGLE_CLASSROOM' },
193
+ { name: 'Google Docs', slug: 'GOOGLEDOCS' },
194
+ { name: 'Google Drive', slug: 'GOOGLEDRIVE' },
195
+ { name: 'Google Maps', slug: 'GOOGLE_MAPS' },
196
+ { name: 'Google Meet', slug: 'GOOGLEMEET' },
197
+ { name: 'Google Photos', slug: 'GOOGLEPHOTOS' },
198
+ { name: 'Google Search Console', slug: 'GOOGLE_SEARCH_CONSOLE' },
199
+ { name: 'Google Sheets', slug: 'GOOGLESHEETS' },
200
+ { name: 'Google Slides', slug: 'GOOGLESLIDES' },
201
+ { name: 'Google Super', slug: 'GOOGLESUPER' },
202
+ { name: 'Google Tasks', slug: 'GOOGLETASKS' },
203
+ { name: 'Gorgias', slug: 'GORGIAS' },
204
+ { name: 'Gumroad', slug: 'GUMROAD' },
205
+ { name: 'Harvest', slug: 'HARVEST' },
206
+ { name: 'HubSpot', slug: 'HUBSPOT' },
207
+ { name: 'Hugging Face', slug: 'HUGGING_FACE' },
208
+ { name: 'Instagram', slug: 'INSTAGRAM' },
209
+ { name: 'Intercom', slug: 'INTERCOM' },
210
+ { name: 'Jira', slug: 'JIRA' },
211
+ { name: 'Kit', slug: 'KIT' },
212
+ { name: 'Linear', slug: 'LINEAR' },
213
+ { name: 'LinkedIn', slug: 'LINKEDIN' },
214
+ { name: 'Linkhut', slug: 'LINKHUT' },
215
+ { name: 'Mailchimp', slug: 'MAILCHIMP' },
216
+ { name: 'Microsoft Teams', slug: 'MICROSOFT_TEAMS' },
217
+ { name: 'Miro', slug: 'MIRO' },
218
+ { name: 'Monday', slug: 'MONDAY' },
219
+ { name: 'Moneybird', slug: 'MONEYBIRD' },
220
+ { name: 'Mural', slug: 'MURAL' },
221
+ { name: 'Notion', slug: 'NOTION' },
222
+ { name: 'Omnisend', slug: 'OMNISEND' },
223
+ { name: 'OneDrive', slug: 'ONE_DRIVE' },
224
+ { name: 'Outlook', slug: 'OUTLOOK' },
225
+ { name: 'PagerDuty', slug: 'PAGERDUTY' },
226
+ { name: 'Prisma', slug: 'PRISMA' },
227
+ { name: 'Productboard', slug: 'PRODUCTBOARD' },
228
+ { name: 'Pushbullet', slug: 'PUSHBULLET' },
229
+ { name: 'QuickBooks', slug: 'QUICKBOOKS' },
230
+ { name: 'Reddit', slug: 'REDDIT' },
231
+ { name: 'Reddit Ads', slug: 'REDDIT_ADS' },
232
+ { name: 'Roam', slug: 'ROAM' },
233
+ { name: 'Salesforce', slug: 'SALESFORCE' },
234
+ { name: 'Sentry', slug: 'SENTRY' },
235
+ { name: 'Servicem8', slug: 'SERVICEM8' },
236
+ { name: 'SharePoint', slug: 'SHARE_POINT' },
237
+ { name: 'Shippo', slug: 'SHIPPO' },
238
+ { name: 'Slack', slug: 'SLACK' },
239
+ { name: 'Slackbot', slug: 'SLACKBOT' },
240
+ { name: 'Splitwise', slug: 'SPLITWISE' },
241
+ { name: 'Square', slug: 'SQUARE' },
242
+ { name: 'Stack Exchange', slug: 'STACK_EXCHANGE' },
243
+ { name: 'Strava', slug: 'STRAVA' },
244
+ { name: 'Stripe', slug: 'STRIPE' },
245
+ { name: 'Supabase', slug: 'SUPABASE' },
246
+ { name: 'Ticketmaster', slug: 'TICKETMASTER' },
247
+ { name: 'Ticktick', slug: 'TICKTICK' },
248
+ { name: 'Timely', slug: 'TIMELY' },
249
+ { name: 'Todoist', slug: 'TODOIST' },
250
+ { name: 'Toneden', slug: 'TONEDEN' },
251
+ { name: 'Trello', slug: 'TRELLO' },
252
+ { name: 'Typeform', slug: 'TYPEFORM' },
253
+ { name: 'WakaTime', slug: 'WAKATIME' },
254
+ { name: 'Webex', slug: 'WEBEX' },
255
+ { name: 'WhatsApp', slug: 'WHATSAPP' },
256
+ { name: 'Wrike', slug: 'WRIKE' },
257
+ { name: 'Yandex', slug: 'YANDEX' },
258
+ { name: 'YNAB', slug: 'YNAB' },
259
+ { name: 'YouTube', slug: 'YOUTUBE' },
260
+ { name: 'Zendesk', slug: 'ZENDESK' },
261
+ { name: 'Zoho', slug: 'ZOHO' },
262
+ { name: 'Zoho Bigin', slug: 'ZOHO_BIGIN' },
263
+ { name: 'Zoho Books', slug: 'ZOHO_BOOKS' },
264
+ { name: 'Zoho Desk', slug: 'ZOHO_DESK' },
265
+ { name: 'Zoho Inventory', slug: 'ZOHO_INVENTORY' },
266
+ { name: 'Zoho Invoice', slug: 'ZOHO_INVOICE' },
267
+ { name: 'Zoho Mail', slug: 'ZOHO_MAIL' },
268
+ { name: 'Zoom', slug: 'ZOOM' },
269
+ { name: 'Apify MCP', slug: 'APIFY_MCP' },
270
+ { name: 'BambooHR', slug: 'BAMBOOHR' },
271
+ { name: 'Beeminder', slug: 'BEEMINDER' },
272
+ { name: 'Bitwarden', slug: 'BITWARDEN' },
273
+ { name: 'Blackboard', slug: 'BLACKBOARD' },
274
+ { name: 'Borneo', slug: 'BORNEO' },
275
+ { name: 'Brevo', slug: 'BREVO' },
276
+ { name: 'Brex', slug: 'BREX' },
277
+ { name: 'Canvas', slug: 'CANVAS' },
278
+ { name: 'Clockify', slug: 'CLOCKIFY' },
279
+ { name: 'Coupa', slug: 'COUPA' },
280
+ { name: 'D2L Brightspace', slug: 'D2LBRIGHTSPACE' },
281
+ { name: 'Databricks', slug: 'DATABRICKS' },
282
+ { name: 'Datadog', slug: 'DATADOG' },
283
+ { name: 'DocuSign', slug: 'DOCUSIGN' },
284
+ { name: 'Dropbox Sign', slug: 'DROPBOX_SIGN' },
285
+ { name: 'Egnyte', slug: 'EGNYTE' },
286
+ { name: 'Epic Games', slug: 'EPIC_GAMES' },
287
+ { name: 'Fly', slug: 'FLY' },
288
+ { name: 'Follow Up Boss', slug: 'FOLLOW_UP_BOSS' },
289
+ { name: 'Gong', slug: 'GONG' },
290
+ { name: 'Google Admin', slug: 'GOOGLE_ADMIN' },
291
+ { name: 'Google Chat', slug: 'GOOGLE_CHAT' },
292
+ { name: 'Googlecontacts', slug: 'GOOGLECONTACTS' },
293
+ { name: 'Googleforms', slug: 'GOOGLEFORMS' },
294
+ { name: 'Granola MCP', slug: 'GRANOLA_MCP' },
295
+ { name: 'Gusto', slug: 'GUSTO' },
296
+ { name: 'Help Scout', slug: 'HELP_SCOUT' },
297
+ { name: 'Highlevel', slug: 'HIGHLEVEL' },
298
+ { name: 'Insighto.ai', slug: 'INSIGHTO_AI' },
299
+ { name: 'Klaviyo', slug: 'KLAVIYO' },
300
+ { name: 'Kommo', slug: 'KOMMO' },
301
+ { name: 'Lever', slug: 'LEVER' },
302
+ { name: 'Linkedin Ads', slug: 'LINKEDIN_ADS' },
303
+ { name: 'Lodgify', slug: 'LODGIFY' },
304
+ { name: 'Matterport', slug: 'MATTERPORT' },
305
+ { name: 'Meta Ads', slug: 'METAADS' },
306
+ { name: 'Monday MCP', slug: 'MONDAY_MCP' },
307
+ { name: 'Netsuite', slug: 'NETSUITE' },
308
+ { name: 'Parma', slug: 'PARMA' },
309
+ { name: 'Pinecone', slug: 'PINECONE' },
310
+ { name: 'Pipedrive', slug: 'PIPEDRIVE' },
311
+ { name: 'Ramp', slug: 'RAMP' },
312
+ { name: 'Razorpay', slug: 'RAZORPAY' },
313
+ { name: 'Recruitee', slug: 'RECRUITEE' },
314
+ { name: 'Salesforce Service Cloud', slug: 'SALESFORCE_SERVICE_CLOUD' },
315
+ { name: 'Scheduleonce', slug: 'SCHEDULEONCE' },
316
+ { name: 'Sendloop', slug: 'SENDLOOP' },
317
+ { name: 'ServiceNow', slug: 'SERVICENOW' },
318
+ { name: 'Shopify', slug: 'SHOPIFY' },
319
+ { name: 'Snapchat', slug: 'SNAPCHAT' },
320
+ { name: 'Snowflake', slug: 'SNOWFLAKE' },
321
+ { name: 'Spotify', slug: 'SPOTIFY' },
322
+ { name: 'Storyblok', slug: 'STORYBLOK' },
323
+ { name: 'SurveyMonkey', slug: 'SURVEY_MONKEY' },
324
+ { name: 'Tally', slug: 'TALLY' },
325
+ { name: 'Tavily MCP', slug: 'TAVILY_MCP' },
326
+ { name: 'Tiktok', slug: 'TIKTOK' },
327
+ { name: 'TinyFish MCP', slug: 'TINYFISH_MCP' },
328
+ { name: 'Twitter', slug: 'TWITTER' },
329
+ { name: 'Webflow', slug: 'WEBFLOW' },
330
+ { name: 'Xero', slug: 'XERO' },
331
+ { name: 'Zoominfo', slug: 'ZOOMINFO' },
332
+ ]);
333
+ /** Runtime guard used when loading untrusted persisted credential records. */
334
+ export function isComposioCredentialMaterial(value) {
335
+ if (!value || typeof value !== 'object' || Array.isArray(value))
336
+ return false;
337
+ const record = value;
338
+ return record.provider === 'composio'
339
+ && Boolean(getString(record.providerConnectionId))
340
+ && Boolean(getString(record.userId))
341
+ && Boolean(getString(record.connectorId))
342
+ && Boolean(getString(record.toolkitSlug))
343
+ && Boolean(getString(record.authConfigId))
344
+ && Boolean(getString(record.validatedAt))
345
+ && (record.accountId === undefined || Boolean(getString(record.accountId)));
346
+ }
347
+ /**
348
+ * Composio adapter for toolkit discovery, connected-account OAuth, and direct
349
+ * tool execution.
350
+ *
351
+ * The instance owns all mutable caches. No config, user, filesystem path, or
352
+ * HTTP implementation is held in module-global state.
353
+ *
354
+ * @example
355
+ * ```ts
356
+ * const provider = new ComposioConnectorProvider({
357
+ * userId: 'user_123',
358
+ * configStore,
359
+ * fetchFn,
360
+ * });
361
+ * const definitions = await provider.listDefinitions();
362
+ * ```
363
+ */
364
+ export class ComposioConnectorProvider {
365
+ userId;
366
+ configStore;
367
+ fetchFn;
368
+ baseUrl;
369
+ userAgent;
370
+ productName;
371
+ curationOverlay;
372
+ timeoutMs;
373
+ now;
374
+ onError;
375
+ staticCatalog;
376
+ catalogCachePath;
377
+ discoveredAuthConfigIds;
378
+ locallyCreatedAuthConfigs = new Map();
379
+ definitionsCache = new Map();
380
+ definitionsPromises = new Map();
381
+ definitionsGeneration = 0;
382
+ authConfigCreationPromises = new Map();
383
+ unsupportedManagedAuthConfigs = new Map();
384
+ pendingConnections = new Map();
385
+ persistedDefinitions;
386
+ persistedFetchedAt;
387
+ refreshTimer;
388
+ refreshTimeout;
389
+ constructor(options) {
390
+ const userId = options.userId.trim();
391
+ if (!userId)
392
+ throw new TypeError('Composio userId must not be empty.');
393
+ if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) {
394
+ throw new TypeError('Composio timeoutMs must be a positive finite number.');
395
+ }
396
+ this.userId = userId;
397
+ this.configStore = options.configStore;
398
+ this.fetchFn = options.fetchFn ?? fetch;
399
+ this.baseUrl = (options.baseUrl ?? DEFAULT_COMPOSIO_BASE_URL).replace(/\/+$/, '');
400
+ this.userAgent = options.userAgent?.trim() || DEFAULT_USER_AGENT;
401
+ this.productName = options.productName?.trim() || 'your workspace';
402
+ this.curationOverlay = options.curationOverlay ?? {};
403
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_COMPOSIO_TIMEOUT_MS;
404
+ this.now = options.now ?? Date.now;
405
+ this.onError = options.onError;
406
+ this.catalogCachePath = options.catalogCachePath === undefined
407
+ ? undefined
408
+ : path.resolve(options.catalogCachePath);
409
+ this.staticCatalog = buildStaticComposioCatalog({
410
+ featuredCatalog: options.featuredCatalog ?? FEATURED_COMPOSIO_CATALOG,
411
+ toolkits: DOCUMENTED_COMPOSIO_TOOLKITS,
412
+ curationOverlay: this.curationOverlay,
413
+ productName: this.productName,
414
+ });
415
+ if (this.catalogCachePath)
416
+ this.loadPersistedCatalogCache(this.catalogCachePath);
417
+ }
418
+ isConfigured(definition) {
419
+ return Boolean(this.getApiKey() && (this.getPersistedAuthConfigId(definition.id) || this.discoveredAuthConfigIds?.[definition.id]));
420
+ }
421
+ clearDiscoveryCache() {
422
+ this.discoveredAuthConfigIds = undefined;
423
+ this.locallyCreatedAuthConfigs.clear();
424
+ this.invalidateDefinitionsCache();
425
+ this.authConfigCreationPromises.clear();
426
+ this.unsupportedManagedAuthConfigs.clear();
427
+ }
428
+ configureCatalogCache(dataDir) {
429
+ this.catalogCachePath = path.resolve(dataDir, 'connectors', 'composio-catalog-cache.json');
430
+ this.loadPersistedCatalogCache(this.catalogCachePath);
431
+ }
432
+ startCatalogRefreshLoop() {
433
+ this.stopCatalogRefreshLoop();
434
+ if (this.isPersistedCatalogStale())
435
+ this.scheduleCatalogRefresh(0);
436
+ this.refreshTimer = setInterval(() => {
437
+ void this.refreshCatalogInBackground();
438
+ }, PERSISTED_CATALOG_REFRESH_MS);
439
+ this.refreshTimer.unref?.();
440
+ }
441
+ stopCatalogRefreshLoop() {
442
+ if (this.refreshTimer) {
443
+ clearInterval(this.refreshTimer);
444
+ this.refreshTimer = undefined;
445
+ }
446
+ if (this.refreshTimeout) {
447
+ clearTimeout(this.refreshTimeout);
448
+ this.refreshTimeout = undefined;
449
+ }
450
+ }
451
+ getFastDefinitions() {
452
+ const definitions = this.persistedDefinitions && this.persistedDefinitions.length > 0
453
+ ? this.persistedDefinitions
454
+ : this.staticCatalog;
455
+ return definitions.map(cloneConnectorDefinition);
456
+ }
457
+ getPersistedCatalogMetadata() {
458
+ return {
459
+ ...(this.persistedFetchedAt === undefined ? {} : { fetchedAt: this.persistedFetchedAt }),
460
+ stale: this.isPersistedCatalogStale(),
461
+ };
462
+ }
463
+ async refreshCatalog(signal) {
464
+ this.invalidateDefinitionsCache();
465
+ const definitions = await this.listDefinitions(signal);
466
+ this.setPersistedDefinitions(definitions, new Date(this.now()).toISOString());
467
+ return definitions;
468
+ }
469
+ invalidateDefinitionsCache() {
470
+ this.definitionsGeneration += 1;
471
+ this.definitionsCache.clear();
472
+ this.definitionsPromises.clear();
473
+ }
474
+ /**
475
+ * Lists metadata-only catalog definitions.
476
+ *
477
+ * Aggregate tool-schema hydration is rejected before provider I/O. Callers
478
+ * must use bounded per-connector preview or current-hydration paths.
479
+ *
480
+ * @complexity Time: O(c), for the bounded static catalog. Space: O(c).
481
+ * @overallScore 98/100 — the retained options field preserves API
482
+ * compatibility while rejecting its former unsafe hydration mode.
483
+ */
484
+ async listDefinitions(signal, options = {}) {
485
+ if (options.hydrateTools) {
486
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'aggregate Composio catalog hydration is unsupported; hydrate one connector at a time', 400);
487
+ }
488
+ const cacheKey = 'metadata';
489
+ const now = this.now();
490
+ const cached = this.definitionsCache.get(cacheKey);
491
+ if (cached && cached.expiresAtMs > now) {
492
+ return cached.definitions.map(cloneConnectorDefinition);
493
+ }
494
+ const existing = this.definitionsPromises.get(cacheKey);
495
+ if (existing)
496
+ return (await existing).map(cloneConnectorDefinition);
497
+ const generation = this.definitionsGeneration;
498
+ const promise = this.fetchDefinitions(signal)
499
+ .then((definitions) => {
500
+ if (this.definitionsGeneration === generation) {
501
+ this.definitionsCache.set(cacheKey, { definitions, expiresAtMs: this.now() + DISCOVERY_CACHE_TTL_MS });
502
+ }
503
+ this.setPersistedDefinitions(definitions, new Date(this.now()).toISOString());
504
+ return definitions;
505
+ })
506
+ .finally(() => {
507
+ if (this.definitionsPromises.get(cacheKey) === promise && this.definitionsGeneration === generation)
508
+ this.definitionsPromises.delete(cacheKey);
509
+ });
510
+ this.definitionsPromises.set(cacheKey, promise);
511
+ return (await promise).map(cloneConnectorDefinition);
512
+ }
513
+ async fetchDefinitions(signal) {
514
+ const apiKey = this.getApiKey();
515
+ const authConfigs = apiKey ? await this.listAuthConfigsSafe(signal) : [];
516
+ const configuredByConnectorId = new Map();
517
+ const discoveredAuthConfigIds = {};
518
+ for (const item of authConfigs) {
519
+ const authConfigId = getComposioAuthConfigId(item);
520
+ const toolkitSlug = getComposioToolkitSlug(item);
521
+ const status = getString(item.status)?.toUpperCase();
522
+ if (!authConfigId || !toolkitSlug || (status && status !== 'ENABLED'))
523
+ continue;
524
+ const connectorId = connectorIdForToolkitSlug(toolkitSlug);
525
+ discoveredAuthConfigIds[connectorId] = authConfigId;
526
+ if (!configuredByConnectorId.has(connectorId))
527
+ configuredByConnectorId.set(connectorId, { authConfigId, toolkitSlug });
528
+ }
529
+ for (const [connectorId, local] of this.locallyCreatedAuthConfigs) {
530
+ discoveredAuthConfigIds[connectorId] = local.authConfigId;
531
+ if (!configuredByConnectorId.has(connectorId))
532
+ configuredByConnectorId.set(connectorId, local);
533
+ }
534
+ this.discoveredAuthConfigIds = discoveredAuthConfigIds;
535
+ const toolkits = apiKey ? await this.listToolkitsSafe(signal) : [];
536
+ const toolkitBySlug = new Map(toolkits.map((toolkit) => [normalizeComposioSlug(getString(toolkit.slug) ?? ''), toolkit]));
537
+ const definitions = await mapWithConcurrency(this.staticCatalog, 8, async (staticDefinition) => {
538
+ const configuredEntry = configuredByConnectorId.get(staticDefinition.id);
539
+ const toolkitSlug = configuredEntry?.toolkitSlug ?? staticDefinition.providerConnectorId ?? staticDefinition.id;
540
+ const toolkit = toolkitBySlug.get(normalizeComposioSlug(toolkitSlug));
541
+ return this.definitionFromToolkit(staticDefinition, toolkitSlug, toolkit, false, signal);
542
+ });
543
+ return definitions;
544
+ }
545
+ scheduleCatalogRefresh(delayMs) {
546
+ if (this.refreshTimeout)
547
+ clearTimeout(this.refreshTimeout);
548
+ const timeout = setTimeout(() => {
549
+ if (this.refreshTimeout === timeout)
550
+ this.refreshTimeout = undefined;
551
+ void this.refreshCatalogInBackground();
552
+ }, delayMs);
553
+ this.refreshTimeout = timeout;
554
+ timeout.unref?.();
555
+ }
556
+ async refreshCatalogInBackground() {
557
+ try {
558
+ if (!this.getApiKey())
559
+ return;
560
+ await this.refreshCatalog();
561
+ }
562
+ catch (error) {
563
+ this.onError?.({ operation: 'catalog-refresh', error });
564
+ }
565
+ }
566
+ isPersistedCatalogStale(now = this.now()) {
567
+ if (!this.persistedFetchedAt)
568
+ return true;
569
+ const fetchedAtMs = Date.parse(this.persistedFetchedAt);
570
+ return !Number.isFinite(fetchedAtMs) || now - fetchedAtMs >= PERSISTED_CATALOG_REFRESH_MS;
571
+ }
572
+ loadPersistedCatalogCache(cachePath) {
573
+ const parsed = readPersistedComposioCatalogCache(cachePath);
574
+ if (!parsed) {
575
+ this.persistedDefinitions = undefined;
576
+ this.persistedFetchedAt = undefined;
577
+ return;
578
+ }
579
+ this.persistedDefinitions = parsed.definitions.map((definition) => cloneConnectorDefinition(definition));
580
+ this.persistedFetchedAt = parsed.fetchedAt;
581
+ if (this.isPersistedCatalogStale() && this.getApiKey())
582
+ this.scheduleCatalogRefresh(0);
583
+ }
584
+ setPersistedDefinitions(definitions, fetchedAt) {
585
+ this.persistedDefinitions = definitions.map((definition) => cloneConnectorDefinition(definition));
586
+ this.persistedFetchedAt = fetchedAt;
587
+ if (!this.catalogCachePath)
588
+ return;
589
+ try {
590
+ writePersistedComposioCatalogCache(this.catalogCachePath, {
591
+ schemaVersion: 1,
592
+ fetchedAt,
593
+ provider: 'composio',
594
+ definitions: this.persistedDefinitions,
595
+ });
596
+ }
597
+ catch (error) {
598
+ this.onError?.({ operation: 'catalog-cache-write', error });
599
+ }
600
+ }
601
+ async getDefinition(connectorId, signal) {
602
+ const discovered = (await this.listDefinitions(signal)).find((definition) => definition.id === connectorId);
603
+ if (discovered)
604
+ return discovered;
605
+ return undefined;
606
+ }
607
+ async getHydratedDefinition(connectorId, signal, options = {}) {
608
+ const metadataDefinition = (await this.listDefinitions(signal)).find((definition) => definition.id === connectorId);
609
+ if (!metadataDefinition)
610
+ return undefined;
611
+ // `listDefinitions` normalizes every entry through
612
+ // `definitionFromToolkit`, which always supplies this provider identity.
613
+ const toolkitSlug = metadataDefinition.providerConnectorId;
614
+ return this.definitionFromToolkit(metadataDefinition, toolkitSlug, undefined, true, signal, {
615
+ ...(options.requireCurrentTools === undefined ? {} : { requireCurrentTools: options.requireCurrentTools }),
616
+ });
617
+ }
618
+ async getPreviewDefinition(connectorId, options) {
619
+ const metadataDefinition = (await this.listDefinitions(options.signal)).find((definition) => definition.id === connectorId);
620
+ if (!metadataDefinition)
621
+ return undefined;
622
+ // The metadata discovery boundary above guarantees provider identity.
623
+ const toolkitSlug = metadataDefinition.providerConnectorId;
624
+ return this.definitionFromToolkit(metadataDefinition, toolkitSlug, undefined, true, options.signal, {
625
+ toolsLimit: options.toolsLimit,
626
+ ...(options.toolsCursor === undefined ? {} : { toolsCursor: options.toolsCursor }),
627
+ });
628
+ }
629
+ async connect(definition, callbackUrl, signal) {
630
+ this.pruneExpiredPendingConnections();
631
+ let authConfig = await this.getOrCreateManagedAuthConfigId(definition, signal);
632
+ const state = crypto.randomBytes(24).toString('base64url');
633
+ const expiresAtMs = this.now() + OAUTH_STATE_TTL_MS;
634
+ const expiresAt = new Date(expiresAtMs).toISOString();
635
+ const callbackUrlWithState = appendOAuthStateToCallbackUrl(callbackUrl, state);
636
+ let response;
637
+ try {
638
+ response = await this.createConnectedAccountLink(authConfig.authConfigId, callbackUrlWithState, signal);
639
+ }
640
+ catch (error) {
641
+ if (!authConfig.fromCache || !isCachedAuthConfigRejection(error))
642
+ throw error;
643
+ this.configStore.deleteAuthConfigId(definition.id);
644
+ authConfig = await this.getOrCreateManagedAuthConfigId(definition, signal, { ignoreCache: true });
645
+ response = await this.createConnectedAccountLink(authConfig.authConfigId, callbackUrlWithState, signal);
646
+ }
647
+ const providerConnectionId = getComposioConnectionId(response);
648
+ const redirectUrl = getString(response.redirect_url) ?? getString(response.redirectUrl);
649
+ const status = getString(response.status)?.toUpperCase();
650
+ if (status === 'ACTIVE' && !providerConnectionId) {
651
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio active account response omitted its connection identity', 502, { connectorId: definition.id });
652
+ }
653
+ this.pendingConnections.set(state, { connectorId: definition.id, state, ...(providerConnectionId ? { providerConnectionId } : {}), expiresAtMs });
654
+ const validatedConnection = status === 'ACTIVE' && providerConnectionId
655
+ ? await this.getValidatedConnectedAccount(definition, providerConnectionId, authConfig.authConfigId, signal)
656
+ : undefined;
657
+ if (validatedConnection)
658
+ this.pendingConnections.delete(state);
659
+ return {
660
+ kind: redirectUrl ? 'redirect_required' : status === 'ACTIVE' ? 'connected' : 'pending',
661
+ ...(redirectUrl ? { redirectUrl } : {}),
662
+ ...(providerConnectionId ? { providerConnectionId } : {}),
663
+ expiresAt,
664
+ ...(validatedConnection ? this.connectionToCredentials(definition, providerConnectionId, validatedConnection) : {}),
665
+ };
666
+ }
667
+ async prepareAuthConfig(definition, signal) {
668
+ if (definition.authentication !== 'composio')
669
+ return { status: 'error', message: 'connector is not backed by Composio' };
670
+ const unsupported = this.unsupportedManagedAuthConfigs.get(definition.id);
671
+ if (unsupported) {
672
+ const authConfigId = await this.getExistingAuthConfigIdForToolkit(definition, signal);
673
+ return authConfigId ? { status: 'ready', authConfigId } : { status: 'custom_required', message: unsupported };
674
+ }
675
+ try {
676
+ const resolution = await this.getOrCreateManagedAuthConfigId(definition, signal);
677
+ return { status: 'ready', authConfigId: resolution.authConfigId };
678
+ }
679
+ catch (error) {
680
+ const message = error instanceof Error ? error.message : String(error);
681
+ const customMessage = getCustomAuthRequiredMessage(error, definition);
682
+ if (customMessage) {
683
+ this.unsupportedManagedAuthConfigs.set(definition.id, customMessage);
684
+ return { status: 'custom_required', message: customMessage };
685
+ }
686
+ if (error instanceof Error && !(error instanceof ConnectorServiceError)) {
687
+ this.onError?.({ operation: 'auth-config-prepare', error });
688
+ return { status: 'error', message: 'Composio auth configuration could not be prepared' };
689
+ }
690
+ return { status: 'error', message };
691
+ }
692
+ }
693
+ cancelPendingConnections(connectorId) {
694
+ this.pruneExpiredPendingConnections();
695
+ let cancelled = 0;
696
+ for (const [state, pending] of this.pendingConnections.entries()) {
697
+ if (pending.connectorId !== connectorId)
698
+ continue;
699
+ this.pendingConnections.delete(state);
700
+ cancelled += 1;
701
+ }
702
+ return cancelled;
703
+ }
704
+ async completeConnection(input) {
705
+ this.pruneExpiredPendingConnections();
706
+ const connectorId = input.definition.id;
707
+ const pending = this.pendingConnections.get(input.state);
708
+ this.pendingConnections.delete(input.state);
709
+ if (!pending || pending.connectorId !== connectorId || pending.expiresAtMs < this.now()) {
710
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio OAuth state is missing or expired', 400, { connectorId });
711
+ }
712
+ if (input.status && input.status.toLowerCase() !== 'success') {
713
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio OAuth did not complete successfully', 400, { connectorId });
714
+ }
715
+ const providerConnectionId = input.providerConnectionId ?? pending.providerConnectionId;
716
+ if (input.providerConnectionId && pending.providerConnectionId && input.providerConnectionId !== pending.providerConnectionId) {
717
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio callback connection id did not match pending connection', 403, { connectorId });
718
+ }
719
+ if (!providerConnectionId) {
720
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio callback did not include a connection id', 400, { connectorId });
721
+ }
722
+ const expectedAuthConfigId = await this.getAuthConfigId(input.definition, input.signal);
723
+ if (!expectedAuthConfigId) {
724
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio OAuth auth configuration is missing', 409, { connectorId });
725
+ }
726
+ const response = await this.getValidatedConnectedAccount(input.definition, providerConnectionId, expectedAuthConfigId, input.signal);
727
+ // Validation above proves both provider fields are present and correspond
728
+ // to this connector before any credential or configuration state is stored.
729
+ this.storeAuthConfigId(input.definition, getString(response.auth_config?.id), getString(response.toolkit?.slug));
730
+ return this.connectionToCredentials(input.definition, providerConnectionId, response);
731
+ }
732
+ pruneExpiredPendingConnections(now = this.now()) {
733
+ for (const [state, pending] of this.pendingConnections.entries()) {
734
+ if (pending.expiresAtMs <= now)
735
+ this.pendingConnections.delete(state);
736
+ }
737
+ }
738
+ async getValidatedConnectedAccount(definition, providerConnectionId, expectedAuthConfigId, signal) {
739
+ const connectorId = definition.id;
740
+ const response = await this.requestJson(`/api/v3/connected_accounts/${encodeURIComponent(providerConnectionId)}`, {
741
+ method: 'GET',
742
+ ...(signal === undefined ? {} : { signal }),
743
+ });
744
+ const returnedConnectionId = getComposioConnectionId(response);
745
+ if (!returnedConnectionId || returnedConnectionId !== providerConnectionId) {
746
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio account identity was missing or mismatched', 502, { connectorId });
747
+ }
748
+ const providerUserId = getString(response.user_id) ?? getString(response.userId);
749
+ if (!providerUserId || providerUserId !== this.getUserId()) {
750
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio account belongs to a different user', 403, { connectorId });
751
+ }
752
+ const providerAuthConfigId = getString(response.auth_config?.id);
753
+ if (!providerAuthConfigId || expectedAuthConfigId !== providerAuthConfigId) {
754
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio account belongs to a different auth configuration', 403, {
755
+ connectorId,
756
+ expectedAuthConfigId,
757
+ providerAuthConfigId: providerAuthConfigId ?? null,
758
+ });
759
+ }
760
+ const expectedToolkitSlug = definition.providerConnectorId;
761
+ if (!expectedToolkitSlug) {
762
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio connector is missing a toolkit slug', 500, { connectorId });
763
+ }
764
+ const providerToolkitSlug = getString(response.toolkit?.slug);
765
+ if (!providerToolkitSlug || connectorIdForToolkitSlug(expectedToolkitSlug) !== connectorIdForToolkitSlug(providerToolkitSlug)) {
766
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio account belongs to a different toolkit', 403, { connectorId });
767
+ }
768
+ const providerStatus = getString(response.status)?.toUpperCase();
769
+ if (providerStatus !== 'ACTIVE') {
770
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio account is not active', 409, {
771
+ connectorId,
772
+ providerStatus: providerStatus ?? null,
773
+ });
774
+ }
775
+ return response;
776
+ }
777
+ requireCredentialForDefinition(definition, credentials) {
778
+ if (!isComposioCredentialMaterial(credentials)) {
779
+ throw new ConnectorServiceError('CONNECTOR_NOT_CONNECTED', 'Composio connector credentials are missing or invalid', 403, {
780
+ connectorId: definition.id,
781
+ });
782
+ }
783
+ const expectedToolkitSlug = definition.providerConnectorId;
784
+ if (credentials.userId !== this.getUserId()
785
+ || credentials.connectorId !== definition.id
786
+ || !expectedToolkitSlug
787
+ || connectorIdForToolkitSlug(credentials.toolkitSlug) !== connectorIdForToolkitSlug(expectedToolkitSlug)) {
788
+ throw new ConnectorServiceError('CONNECTOR_NOT_CONNECTED', 'Composio connector credentials do not match this user and connector', 403, {
789
+ connectorId: definition.id,
790
+ });
791
+ }
792
+ return credentials;
793
+ }
794
+ /**
795
+ * Checks persisted credential identity without contacting Composio.
796
+ *
797
+ * @returns `true` only for credential evidence owned by this provider user
798
+ * and the supplied connector definition.
799
+ *
800
+ * @complexity Time: O(1). Space: O(1).
801
+ * @overallScore 100/100
802
+ */
803
+ credentialMatchesDefinition(definition, credentials) {
804
+ try {
805
+ this.requireCredentialForDefinition(definition, credentials);
806
+ return true;
807
+ }
808
+ catch (error) {
809
+ if (error instanceof ConnectorServiceError)
810
+ return false;
811
+ throw error;
812
+ }
813
+ }
814
+ async revalidateCredential(definition, credentials, signal) {
815
+ const credential = this.requireCredentialForDefinition(definition, credentials);
816
+ await this.getValidatedConnectedAccount(definition, credential.providerConnectionId, credential.authConfigId, signal);
817
+ return credential;
818
+ }
819
+ async disconnect(definition, credentials, signal) {
820
+ if (credentials === undefined)
821
+ return;
822
+ const credential = await this.revalidateCredential(definition, credentials, signal);
823
+ const providerConnectionId = credential.providerConnectionId;
824
+ const response = await this.request(`/api/v3/connected_accounts/${encodeURIComponent(providerConnectionId)}`, { method: 'DELETE', ...(signal === undefined ? {} : { signal }) });
825
+ if (!response.ok && response.status !== 404) {
826
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', `Composio disconnect failed with HTTP ${response.status}`, 502, { httpStatus: response.status });
827
+ }
828
+ }
829
+ async execute(definition, tool, input, credentials, signal) {
830
+ const declaredTool = definition.tools.find((candidate) => (candidate.name === tool.name
831
+ && candidate.providerToolId === tool.providerToolId));
832
+ const runtimeSafety = classifyConnectorToolSafety(tool);
833
+ if (!declaredTool
834
+ || !definition.allowedToolNames.includes(tool.name)
835
+ || definition.minimumApproval !== 'auto'
836
+ || tool.safety.sideEffect !== 'read'
837
+ || tool.safety.approval !== 'auto'
838
+ || runtimeSafety.sideEffect !== 'read'
839
+ || runtimeSafety.approval !== 'auto') {
840
+ throw new ConnectorServiceError('CONNECTOR_SAFETY_DENIED', 'Composio provider refused a tool that is not proven read-only', 403, {
841
+ connectorId: definition.id,
842
+ toolName: tool.name,
843
+ });
844
+ }
845
+ try {
846
+ if (tool.inputSchemaUnsupportedReason !== undefined)
847
+ throw new Error(tool.inputSchemaUnsupportedReason);
848
+ assertConnectorInputMatchesSchema(input, tool.inputSchemaJson);
849
+ }
850
+ catch (error) {
851
+ // Both the unsupported-schema guard and schema validator throw Error.
852
+ throw new ConnectorServiceError('CONNECTOR_INPUT_SCHEMA_MISMATCH', error.message, 400, {
853
+ connectorId: definition.id,
854
+ toolName: tool.name,
855
+ });
856
+ }
857
+ const credential = await this.revalidateCredential(definition, credentials, signal);
858
+ const providerConnectionId = credential.providerConnectionId;
859
+ const providerToolId = tool.providerToolId ?? tool.name;
860
+ const response = await this.requestJson(`/api/v3.1/tools/execute/${encodeURIComponent(providerToolId)}`, {
861
+ method: 'POST',
862
+ body: JSON.stringify({
863
+ connected_account_id: providerConnectionId,
864
+ user_id: this.getUserId(),
865
+ version: 'latest',
866
+ arguments: input,
867
+ }),
868
+ ...(signal === undefined ? {} : { signal }),
869
+ });
870
+ if (response.successful !== true || response.error) {
871
+ const authStale = isComposioAuthenticationFailure(response.error);
872
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio tool execution failed', 502, {
873
+ connectorId: definition.id,
874
+ toolName: tool.name,
875
+ ...(authStale ? { authStale: true } : {}),
876
+ });
877
+ }
878
+ const output = toStructurallyBoundedJsonValue(response.data);
879
+ const result = {
880
+ toolName: tool.name,
881
+ providerToolId,
882
+ data: output,
883
+ ...(getString(response.log_id) ?? getString(response.logId) ? { providerExecutionId: (getString(response.log_id) ?? getString(response.logId)) } : {}),
884
+ ...(response.session_info === undefined && response.sessionInfo === undefined
885
+ ? {}
886
+ : { sessionInfo: toStructurallyBoundedJsonValue(response.session_info ?? response.sessionInfo) }),
887
+ };
888
+ const protectedResult = protectConnectorOutput(result);
889
+ // The object overload on the centralized protection boundary guarantees
890
+ // that bounding and redaction preserve the root object container.
891
+ return protectedResult.output;
892
+ }
893
+ async getAuthConfigId(definition, signal) {
894
+ const persisted = this.getPersistedAuthConfigId(definition.id);
895
+ if (persisted)
896
+ return persisted;
897
+ if (!this.discoveredAuthConfigIds)
898
+ this.discoveredAuthConfigIds = await this.discoverAuthConfigIds(signal);
899
+ return this.discoveredAuthConfigIds[definition.id];
900
+ }
901
+ async getOrCreateManagedAuthConfigId(definition, signal, options = {}) {
902
+ if (!options.ignoreCache) {
903
+ const persisted = this.getPersistedAuthConfigId(definition.id);
904
+ if (persisted) {
905
+ return { authConfigId: persisted, fromCache: true };
906
+ }
907
+ const discovered = this.discoveredAuthConfigIds?.[definition.id];
908
+ if (discovered) {
909
+ return { authConfigId: discovered, fromCache: true };
910
+ }
911
+ }
912
+ const existing = await this.getAuthConfigIdForToolkit(definition, signal);
913
+ if (existing) {
914
+ this.storeAuthConfigId(definition, existing);
915
+ return { authConfigId: existing, fromCache: false };
916
+ }
917
+ const inFlight = this.authConfigCreationPromises.get(definition.id);
918
+ if (inFlight) {
919
+ const authConfigId = await inFlight;
920
+ return { authConfigId, fromCache: false };
921
+ }
922
+ const promise = this.createAndStoreManagedAuthConfigId(definition, signal)
923
+ .finally(() => {
924
+ if (this.authConfigCreationPromises.get(definition.id) === promise)
925
+ this.authConfigCreationPromises.delete(definition.id);
926
+ });
927
+ this.authConfigCreationPromises.set(definition.id, promise);
928
+ const authConfigId = await promise;
929
+ return { authConfigId, fromCache: false };
930
+ }
931
+ async createAndStoreManagedAuthConfigId(definition, signal) {
932
+ const created = await this.createManagedAuthConfig(definition, signal);
933
+ const authConfigId = getComposioAuthConfigId(created);
934
+ const toolkitSlug = getComposioToolkitSlug(created) ?? definition.providerConnectorId;
935
+ if (!authConfigId || !toolkitSlug) {
936
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio auth config response was missing an id or toolkit slug', 502, {
937
+ connectorId: definition.id,
938
+ });
939
+ }
940
+ const connectorId = connectorIdForToolkitSlug(toolkitSlug);
941
+ if (connectorId !== definition.id) {
942
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio created an auth config for a different toolkit', 502, {
943
+ connectorId: definition.id,
944
+ toolkitSlug,
945
+ });
946
+ }
947
+ this.storeAuthConfigId(definition, authConfigId, toolkitSlug);
948
+ this.invalidateDefinitionsCache();
949
+ return authConfigId;
950
+ }
951
+ async createManagedAuthConfig(definition, signal) {
952
+ // This method is reached only after `getAuthConfigIdForToolkit` has
953
+ // required the same definition toolkit. Keep one fail-closed guard at that
954
+ // boundary instead of an unreachable duplicate after the provider lookup.
955
+ const toolkitSlug = definition.providerConnectorId;
956
+ try {
957
+ return await this.requestJson('/api/v3.1/auth_configs', {
958
+ method: 'POST',
959
+ body: JSON.stringify({
960
+ toolkit: { slug: toolkitSlug },
961
+ auth_config: { type: 'use_composio_managed_auth' },
962
+ }),
963
+ ...(signal === undefined ? {} : { signal }),
964
+ });
965
+ }
966
+ catch (error) {
967
+ const customMessage = getCustomAuthRequiredMessage(error, definition);
968
+ if (!customMessage)
969
+ throw error;
970
+ this.unsupportedManagedAuthConfigs.set(definition.id, customMessage);
971
+ throw new ConnectorServiceError('CONNECTOR_AUTH_CONFIG_REQUIRED', customMessage, 409, {
972
+ connectorId: definition.id,
973
+ provider: 'composio',
974
+ reason: 'managed_auth_unavailable',
975
+ });
976
+ }
977
+ }
978
+ async getAuthConfigIdForToolkit(definition, signal) {
979
+ const toolkitSlug = definition.providerConnectorId;
980
+ if (!toolkitSlug) {
981
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio connector is missing a toolkit slug', 500, { connectorId: definition.id });
982
+ }
983
+ const items = await this.listAuthConfigsSafe(signal, toolkitSlug);
984
+ for (const item of items) {
985
+ const authConfigId = getComposioAuthConfigId(item);
986
+ const itemToolkitSlug = getComposioToolkitSlug(item) ?? toolkitSlug;
987
+ const status = getString(item.status)?.toUpperCase();
988
+ if (!authConfigId || (status && status !== 'ENABLED'))
989
+ continue;
990
+ if (connectorIdForToolkitSlug(itemToolkitSlug) !== definition.id)
991
+ continue;
992
+ return authConfigId;
993
+ }
994
+ return undefined;
995
+ }
996
+ async getExistingAuthConfigIdForToolkit(definition, signal) {
997
+ const persisted = this.getPersistedAuthConfigId(definition.id);
998
+ if (persisted)
999
+ return persisted;
1000
+ const discovered = this.discoveredAuthConfigIds?.[definition.id];
1001
+ if (discovered)
1002
+ return discovered;
1003
+ const existing = await this.getAuthConfigIdForToolkit(definition, signal);
1004
+ if (!existing)
1005
+ return undefined;
1006
+ this.storeAuthConfigId(definition, existing);
1007
+ return existing;
1008
+ }
1009
+ async createConnectedAccountLink(authConfigId, callbackUrl, signal) {
1010
+ return this.requestJson('/api/v3/connected_accounts/link', {
1011
+ method: 'POST',
1012
+ body: JSON.stringify({
1013
+ auth_config_id: authConfigId,
1014
+ user_id: this.getUserId(),
1015
+ callback_url: callbackUrl,
1016
+ }),
1017
+ ...(signal === undefined ? {} : { signal }),
1018
+ });
1019
+ }
1020
+ getPersistedAuthConfigId(connectorId) {
1021
+ return getString(this.configStore.read().authConfigIds[connectorId]);
1022
+ }
1023
+ storeAuthConfigId(definition, authConfigId, toolkitSlug = definition.providerConnectorId) {
1024
+ this.discoveredAuthConfigIds = {
1025
+ ...(this.discoveredAuthConfigIds ?? {}),
1026
+ [definition.id]: authConfigId,
1027
+ };
1028
+ if (toolkitSlug)
1029
+ this.locallyCreatedAuthConfigs.set(definition.id, { authConfigId, toolkitSlug });
1030
+ this.configStore.setAuthConfigId(definition.id, authConfigId);
1031
+ }
1032
+ async discoverAuthConfigIds(signal) {
1033
+ if (!this.getApiKey())
1034
+ return {};
1035
+ const items = await this.listAuthConfigsSafe(signal);
1036
+ const discovered = {};
1037
+ for (const item of items) {
1038
+ const authConfigId = getComposioAuthConfigId(item);
1039
+ const toolkitSlug = getComposioToolkitSlug(item);
1040
+ const status = getString(item.status)?.toUpperCase();
1041
+ if (!authConfigId || !toolkitSlug || (status && status !== 'ENABLED'))
1042
+ continue;
1043
+ discovered[connectorIdForToolkitSlug(toolkitSlug)] = authConfigId;
1044
+ }
1045
+ // Locally-created entries already initialize `discoveredAuthConfigIds`.
1046
+ // `discoverAuthConfigIds` is called only while that map is undefined, so a
1047
+ // local-entry merge here could never observe an entry.
1048
+ return discovered;
1049
+ }
1050
+ async listAuthConfigs(signal, toolkitSlug) {
1051
+ const searchParams = new URLSearchParams({ limit: '1000' });
1052
+ if (toolkitSlug)
1053
+ searchParams.set('toolkit_slug', toolkitSlug);
1054
+ const path = `/api/v3.1/auth_configs?${searchParams.toString()}`;
1055
+ const response = await this.request(path, { method: 'GET', ...(signal === undefined ? {} : { signal }) });
1056
+ if (!response.ok)
1057
+ return [];
1058
+ const payload = await readBoundedResponseJson(response);
1059
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
1060
+ return [];
1061
+ const record = payload;
1062
+ const items = Array.isArray(record.items) ? record.items : Array.isArray(record.data) ? record.data : [];
1063
+ return items.slice(0, COMPOSIO_MAX_LIST_ITEMS).filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item)));
1064
+ }
1065
+ async listAuthConfigsSafe(signal, toolkitSlug) {
1066
+ try {
1067
+ return await this.listAuthConfigs(signal, toolkitSlug);
1068
+ }
1069
+ catch {
1070
+ return [];
1071
+ }
1072
+ }
1073
+ async listToolkits(signal) {
1074
+ const response = await this.request('/api/v3.1/toolkits?limit=1000', { method: 'GET', ...(signal === undefined ? {} : { signal }) });
1075
+ if (!response.ok)
1076
+ return [];
1077
+ const payload = await readBoundedResponseJson(response);
1078
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
1079
+ return [];
1080
+ const record = payload;
1081
+ const items = Array.isArray(record.items) ? record.items : Array.isArray(record.data) ? record.data : [];
1082
+ return items.slice(0, COMPOSIO_MAX_LIST_ITEMS).filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item)));
1083
+ }
1084
+ async listToolkitsSafe(signal) {
1085
+ try {
1086
+ return await this.listToolkits(signal);
1087
+ }
1088
+ catch {
1089
+ return [];
1090
+ }
1091
+ }
1092
+ async listToolsPage(toolkitSlug, options) {
1093
+ const searchParams = new URLSearchParams({
1094
+ toolkit_slug: toolkitSlug.toLowerCase(),
1095
+ toolkit_versions: 'latest',
1096
+ limit: String(options.limit),
1097
+ });
1098
+ if (options.cursor)
1099
+ searchParams.set('cursor', options.cursor);
1100
+ const response = await this.request(`/api/v3.1/tools?${searchParams.toString()}`, { method: 'GET', ...(options.signal === undefined ? {} : { signal: options.signal }) });
1101
+ if (!response.ok) {
1102
+ const message = await getComposioSafeErrorMessage(response);
1103
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', message ?? `Composio tools request failed with HTTP ${response.status}`, response.status === 401 ? 401 : 502, { httpStatus: response.status });
1104
+ }
1105
+ const payload = await readBoundedResponseJson(response);
1106
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
1107
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio tools response was invalid', 502);
1108
+ }
1109
+ const record = payload;
1110
+ const items = Array.isArray(record.items) ? record.items : Array.isArray(record.data) ? record.data : [];
1111
+ const nextCursor = getString(record.next_cursor) ?? getString(record.nextCursor);
1112
+ const totalItems = getNonNegativeInteger(record.total_items) ?? getNonNegativeInteger(record.totalItems);
1113
+ return {
1114
+ items: items.slice(0, COMPOSIO_MAX_LIST_ITEMS).filter((item) => Boolean(item && typeof item === 'object' && !Array.isArray(item))),
1115
+ ...(nextCursor === undefined ? {} : { nextCursor }),
1116
+ ...(totalItems === undefined ? {} : { totalItems }),
1117
+ };
1118
+ }
1119
+ async listTools(toolkitSlug, signal) {
1120
+ return (await this.listToolsPage(toolkitSlug, { limit: 1000, ...(signal === undefined ? {} : { signal }) })).items;
1121
+ }
1122
+ async listToolsSafe(toolkitSlug, signal) {
1123
+ try {
1124
+ return await this.listTools(toolkitSlug, signal);
1125
+ }
1126
+ catch {
1127
+ return [];
1128
+ }
1129
+ }
1130
+ /**
1131
+ * Reconciles one static connector with bounded current provider metadata.
1132
+ *
1133
+ * Strict execution mode removes static authority when the successful
1134
+ * current response omits a tool. Discovered tools remain display-only unless
1135
+ * an exact package or host curation entry grants authority.
1136
+ *
1137
+ * @complexity Time: O(s + l). Space: O(s + l), for static and live tools.
1138
+ * @overallScore 96/100 — authority reconciliation is explicit but remains a
1139
+ * dense provider-adapter boundary that warrants contract-test coverage.
1140
+ */
1141
+ async definitionFromToolkit(staticDefinition, toolkitSlug, toolkit, hydrateTools, signal, toolPageOptions = {}) {
1142
+ const connectorId = staticDefinition.id;
1143
+ const toolPage = hydrateTools && toolPageOptions.toolsLimit !== undefined
1144
+ ? await this.listToolsPage(toolkitSlug, {
1145
+ limit: toolPageOptions.toolsLimit,
1146
+ ...(toolPageOptions.toolsCursor === undefined ? {} : { cursor: toolPageOptions.toolsCursor }),
1147
+ ...(signal === undefined ? {} : { signal }),
1148
+ })
1149
+ : undefined;
1150
+ const discoveredLiveTools = hydrateTools
1151
+ ? (toolPage?.items
1152
+ ?? (toolPageOptions.requireCurrentTools
1153
+ ? await this.listTools(toolkitSlug, signal)
1154
+ : await this.listToolsSafe(toolkitSlug, signal)))
1155
+ .filter((tool) => {
1156
+ const toolToolkitSlug = getString(tool.toolkit?.slug);
1157
+ return !toolToolkitSlug || normalizeComposioSlug(toolToolkitSlug) === normalizeComposioSlug(toolkitSlug);
1158
+ })
1159
+ .map((tool) => this.toolDefinitionFromComposioTool(connectorId, tool))
1160
+ : [];
1161
+ const liveTools = discoveredLiveTools.map((tool) => (this.isStaticToolDeclaration(staticDefinition, tool)
1162
+ || this.isExplicitlyAllowedLiveTool(connectorId, tool)
1163
+ ? tool
1164
+ : { ...tool, refreshEligible: false }));
1165
+ const liveToolsByName = new Map(liveTools.map((tool) => [tool.name, tool]));
1166
+ const liveToolsByProviderId = new Map(liveTools.map((tool) => [normalizeProviderToolId(tool.providerToolId), tool]));
1167
+ const matchedLiveToolNames = new Set();
1168
+ const matchedStaticToolNames = new Set();
1169
+ const mergedStaticTools = staticDefinition.tools.map((tool) => {
1170
+ const liveTool = liveToolsByName.get(tool.name)
1171
+ ?? (tool.providerToolId === undefined
1172
+ ? undefined
1173
+ : liveToolsByProviderId.get(normalizeProviderToolId(tool.providerToolId)));
1174
+ if (liveTool !== undefined) {
1175
+ matchedLiveToolNames.add(liveTool.name);
1176
+ matchedStaticToolNames.add(tool.name);
1177
+ }
1178
+ return mergeToolDefinition(tool, liveTool);
1179
+ });
1180
+ const tools = [
1181
+ ...mergedStaticTools,
1182
+ ...liveTools.filter((tool) => !matchedLiveToolNames.has(tool.name)),
1183
+ ];
1184
+ const currentStaticAllowedToolNames = toolPageOptions.requireCurrentTools
1185
+ ? staticDefinition.allowedToolNames.filter((toolName) => matchedStaticToolNames.has(toolName))
1186
+ : staticDefinition.allowedToolNames;
1187
+ const explicitlyAllowedLiveToolNames = liveTools
1188
+ .filter((tool) => tool.refreshEligible && this.isExplicitlyAllowedLiveTool(connectorId, tool))
1189
+ .map((tool) => tool.name);
1190
+ const allowedToolNames = [...new Set([
1191
+ ...currentStaticAllowedToolNames,
1192
+ ...explicitlyAllowedLiveToolNames,
1193
+ ])];
1194
+ // `curatedToolNames` mirrors the static catalog ONLY — it
1195
+ // intentionally never picks up hydrated live tool names. It
1196
+ // preserves the static catalog baseline, while summary badges use
1197
+ // `toolCount` when present to reflect the advertised provider
1198
+ // inventory. The execution-time gate keeps using
1199
+ // `allowedToolNames`, where only exact package/host-owned entries can
1200
+ // extend the static execution authority.
1201
+ const curatedToolNames = [...staticDefinition.allowedToolNames];
1202
+ const name = getString(toolkit?.name) ?? staticDefinition.name;
1203
+ const category = firstCategoryName(toolkit?.meta?.categories) ?? firstCategoryName(toolkit?.categories) ?? staticDefinition.category;
1204
+ const liveDescription = getComposioToolkitDescription(toolkit);
1205
+ const description = liveDescription ?? staticDefinition.description;
1206
+ const liveToolCount = getComposioToolkitToolCount(toolkit);
1207
+ const toolCount = toolPage?.totalItems ?? liveToolCount ?? staticDefinition.toolCount ?? (tools.length > 0 ? tools.length : undefined);
1208
+ return {
1209
+ ...staticDefinition,
1210
+ id: connectorId,
1211
+ name,
1212
+ providerConnectorId: staticDefinition.providerConnectorId ?? toolkitSlug,
1213
+ category,
1214
+ ...(description === undefined ? {} : { description }),
1215
+ tools,
1216
+ ...(toolCount === undefined ? {} : { toolCount }),
1217
+ ...(toolPage?.nextCursor === undefined ? {} : { toolsNextCursor: toolPage.nextCursor }),
1218
+ ...(toolPage === undefined ? {} : { toolsHasMore: toolPage.nextCursor !== undefined }),
1219
+ allowedToolNames,
1220
+ curatedToolNames,
1221
+ ...(staticDefinition.featuredToolNames === undefined
1222
+ ? tools.length > 0 ? { featuredToolNames: tools.slice(0, 3).map((tool) => tool.name) } : {}
1223
+ : { featuredToolNames: staticDefinition.featuredToolNames }),
1224
+ };
1225
+ }
1226
+ isExplicitlyAllowedLiveTool(connectorId, tool) {
1227
+ const connectorKey = normalizeComposioSlug(connectorId);
1228
+ const toolKey = normalizeProviderToolId(tool.providerToolId);
1229
+ return COMPOSIO_STATIC_DISCOVERED_TOOL_ALLOWLIST.has(`${connectorKey}:${toolKey}`)
1230
+ || this.curationOverlay[connectorKey]?.[toolKey] !== undefined;
1231
+ }
1232
+ isStaticToolDeclaration(definition, liveTool) {
1233
+ return definition.tools.some((staticTool) => (definition.allowedToolNames.includes(staticTool.name)
1234
+ && (staticTool.name === liveTool.name
1235
+ || (staticTool.providerToolId !== undefined
1236
+ && normalizeProviderToolId(staticTool.providerToolId)
1237
+ === normalizeProviderToolId(liveTool.providerToolId)))));
1238
+ }
1239
+ toolDefinitionFromComposioTool(connectorId, tool) {
1240
+ const providerToolId = getString(tool.slug) ?? getString(tool.name) ?? `${connectorId.toUpperCase()}_TOOL`;
1241
+ const description = getString(tool.description) ?? getString(tool.human_description) ?? getString(tool.humanDescription) ?? '';
1242
+ const requiredScopes = getStringArray(tool.scopes ?? tool.oauth_scopes ?? tool.oauthScopes ?? tool.auth_scopes ?? tool.authScopes ?? tool.tags);
1243
+ const decodedInputSchema = decodeComposioInputParameters(tool.input_parameters ?? tool.inputParameters);
1244
+ return {
1245
+ ...applyComposioToolCuration(defineConnectorTool({
1246
+ name: `${connectorId}.${normalizeToolName(providerToolId)}`,
1247
+ providerToolId,
1248
+ title: getString(tool.name) ?? titleFromSlug(providerToolId),
1249
+ ...(description ? { description } : {}),
1250
+ inputSchemaJson: decodedInputSchema.schema,
1251
+ inputSchemaUnsupportedReason: decodedInputSchema.unsupportedReason,
1252
+ outputSchemaJson: { type: 'object', additionalProperties: true },
1253
+ requiredScopes,
1254
+ }), connectorId, providerToolId, this.curationOverlay),
1255
+ providerToolId,
1256
+ };
1257
+ }
1258
+ connectionToCredentials(definition, providerConnectionId, response) {
1259
+ const accountLabel = getString(response.account_label)
1260
+ ?? getString(response.accountLabel)
1261
+ ?? getString(response.email)
1262
+ ?? getString(response.name)
1263
+ ?? providerConnectionId;
1264
+ const accountId = getString(response.account_id) ?? getString(response.accountId);
1265
+ // Every caller first passes the response through
1266
+ // `getValidatedConnectedAccount`, which proves these fields are present.
1267
+ const authConfigId = getString(response.auth_config?.id);
1268
+ const toolkitSlug = getString(response.toolkit?.slug);
1269
+ const userId = (getString(response.user_id) ?? getString(response.userId));
1270
+ return {
1271
+ connectorId: definition.id,
1272
+ accountLabel,
1273
+ credentials: {
1274
+ provider: 'composio',
1275
+ providerConnectionId,
1276
+ userId,
1277
+ connectorId: definition.id,
1278
+ toolkitSlug,
1279
+ authConfigId,
1280
+ validatedAt: new Date(this.now()).toISOString(),
1281
+ ...(accountId ? { accountId } : {}),
1282
+ },
1283
+ };
1284
+ }
1285
+ async requestJson(path, input) {
1286
+ const response = await this.request(path, input);
1287
+ if (!response.ok) {
1288
+ const message = await getComposioSafeErrorMessage(response);
1289
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', message ?? `Composio request failed with HTTP ${response.status}`, response.status === 401 ? 401 : 502, { httpStatus: response.status });
1290
+ }
1291
+ let value;
1292
+ try {
1293
+ value = await readBoundedResponseJson(response, COMPOSIO_MAX_RESPONSE_BYTES);
1294
+ }
1295
+ catch (error) {
1296
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', error instanceof BoundedDataError && /exceeds/.test(error.message)
1297
+ ? 'Composio response exceeded safety limits'
1298
+ : 'Composio returned malformed JSON', 502);
1299
+ }
1300
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1301
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio returned an invalid response', 502);
1302
+ }
1303
+ return value;
1304
+ }
1305
+ async request(path, input) {
1306
+ const apiKey = this.getApiKey();
1307
+ if (!apiKey) {
1308
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio provider is not configured', 503, { setting: 'apiKey' });
1309
+ }
1310
+ const timeout = AbortSignal.timeout(this.timeoutMs);
1311
+ const signal = input.signal ? AbortSignal.any([input.signal, timeout]) : timeout;
1312
+ try {
1313
+ return await this.fetchFn(`${this.getBaseUrl()}${path}`, {
1314
+ method: input.method,
1315
+ headers: {
1316
+ accept: 'application/json',
1317
+ 'content-type': 'application/json',
1318
+ 'user-agent': this.userAgent,
1319
+ 'x-api-key': apiKey,
1320
+ },
1321
+ ...(input.body ? { body: input.body } : {}),
1322
+ signal,
1323
+ });
1324
+ }
1325
+ catch (error) {
1326
+ const aborted = signal.aborted || (error instanceof Error && error.name === 'AbortError');
1327
+ throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', aborted ? 'Composio request was cancelled or timed out' : 'Composio request failed', aborted ? 504 : 502);
1328
+ }
1329
+ }
1330
+ getApiKey() {
1331
+ return this.configStore.read().apiKey || undefined;
1332
+ }
1333
+ getBaseUrl() {
1334
+ return this.baseUrl;
1335
+ }
1336
+ getUserId() {
1337
+ return this.userId;
1338
+ }
1339
+ }
1340
+ /** @internal Package-local normalization contract; not re-exported from the package root. */
1341
+ export function mergeToolDefinition(staticTool, liveTool) {
1342
+ if (!liveTool)
1343
+ return staticTool;
1344
+ const merged = {
1345
+ ...staticTool,
1346
+ ...(liveTool.description === undefined ? {} : { description: liveTool.description }),
1347
+ ...(liveTool.inputSchemaJson === undefined ? {} : { inputSchemaJson: liveTool.inputSchemaJson }),
1348
+ ...(liveTool.outputSchemaJson === undefined ? {} : { outputSchemaJson: liveTool.outputSchemaJson }),
1349
+ ...(liveTool.providerToolId === undefined ? {} : { providerToolId: liveTool.providerToolId }),
1350
+ requiredScopes: liveTool.requiredScopes.length > 0 ? liveTool.requiredScopes : staticTool.requiredScopes,
1351
+ safety: liveTool.safety,
1352
+ refreshEligible: liveTool.refreshEligible,
1353
+ ...((liveTool.curation ?? staticTool.curation) === undefined ? {} : { curation: liveTool.curation ?? staticTool.curation }),
1354
+ };
1355
+ if (liveTool.inputSchemaUnsupportedReason === undefined)
1356
+ delete merged.inputSchemaUnsupportedReason;
1357
+ else
1358
+ merged.inputSchemaUnsupportedReason = liveTool.inputSchemaUnsupportedReason;
1359
+ return merged;
1360
+ }
1361
+ function buildStaticComposioCatalog(options) {
1362
+ const definitions = new Map();
1363
+ for (const definition of options.featuredCatalog) {
1364
+ definitions.set(definition.id, {
1365
+ ...definition,
1366
+ provider: 'composio',
1367
+ authentication: 'composio',
1368
+ tools: definition.tools.map((tool) => applyComposioToolCuration(tool, definition.providerConnectorId ?? definition.id, tool.providerToolId, options.curationOverlay)),
1369
+ });
1370
+ }
1371
+ for (const toolkit of options.toolkits) {
1372
+ const id = connectorIdForToolkitSlug(toolkit.slug);
1373
+ if (definitions.has(id))
1374
+ continue;
1375
+ definitions.set(id, createComposioCatalogDefinition(toolkit, options.productName));
1376
+ }
1377
+ return [...definitions.values()];
1378
+ }
1379
+ function createComposioCatalogDefinition(toolkit, productName) {
1380
+ const curated = getComposioToolkitMetadata(toolkit.slug);
1381
+ // Prefer a hand-authored description when offline. Live Composio toolkit
1382
+ // metadata still wins during discovery, as long as it is not the legacy
1383
+ // generic "Connect to X through Composio." placeholder.
1384
+ // Curated metadata requires a description, so the fallback path necessarily
1385
+ // has no curated record and uses the inventory category directly.
1386
+ const description = curated?.description
1387
+ ?? fallbackComposioDescription(toolkit.name, toolkit.category, productName);
1388
+ const category = curated?.category ?? toolkit.category ?? 'Integration';
1389
+ return {
1390
+ id: connectorIdForToolkitSlug(toolkit.slug),
1391
+ name: toolkit.name,
1392
+ provider: 'composio',
1393
+ category,
1394
+ description,
1395
+ providerConnectorId: toolkit.slug,
1396
+ authentication: 'composio',
1397
+ tools: [],
1398
+ allowedToolNames: [],
1399
+ minimumApproval: 'auto',
1400
+ ...(curated?.toolCount === undefined ? {} : { toolCount: curated.toolCount }),
1401
+ };
1402
+ }
1403
+ /**
1404
+ * Builds a detached, offline catalog without network or filesystem I/O.
1405
+ *
1406
+ * @overallScore 100/100
1407
+ */
1408
+ export function getStaticComposioCatalogDefinitions(options = {}) {
1409
+ return buildStaticComposioCatalog({
1410
+ toolkits: options.toolkits ?? DOCUMENTED_COMPOSIO_TOOLKITS,
1411
+ featuredCatalog: options.featuredCatalog ?? FEATURED_COMPOSIO_CATALOG,
1412
+ curationOverlay: options.curationOverlay ?? {},
1413
+ productName: options.productName?.trim() || 'your workspace',
1414
+ }).map(cloneConnectorDefinition);
1415
+ }
1416
+ /** @internal Package-local normalization contract; not re-exported from the package root. */
1417
+ export function cloneConnectorDefinition(definition) {
1418
+ return {
1419
+ ...definition,
1420
+ tools: definition.tools.map((tool) => ({
1421
+ name: tool.name,
1422
+ title: tool.title,
1423
+ ...(tool.description === undefined ? {} : { description: tool.description }),
1424
+ ...(tool.inputSchemaJson === undefined ? {} : { inputSchemaJson: toBoundedJsonObject(tool.inputSchemaJson) }),
1425
+ ...(tool.inputSchemaUnsupportedReason === undefined ? {} : { inputSchemaUnsupportedReason: tool.inputSchemaUnsupportedReason }),
1426
+ ...(tool.outputSchemaJson === undefined ? {} : { outputSchemaJson: toBoundedJsonObject(tool.outputSchemaJson) }),
1427
+ safety: { ...tool.safety },
1428
+ refreshEligible: tool.refreshEligible,
1429
+ ...(tool.curation === undefined ? {} : { curation: { ...(tool.curation.useCases === undefined ? {} : { useCases: [...tool.curation.useCases] }), ...(tool.curation.reason === undefined ? {} : { reason: tool.curation.reason }) } }),
1430
+ requiredScopes: [...tool.requiredScopes],
1431
+ ...(tool.providerToolId === undefined ? {} : { providerToolId: tool.providerToolId }),
1432
+ })),
1433
+ allowedToolNames: [...definition.allowedToolNames],
1434
+ ...(definition.curatedToolNames === undefined ? {} : { curatedToolNames: [...definition.curatedToolNames] }),
1435
+ ...(definition.toolCount === undefined ? {} : { toolCount: definition.toolCount }),
1436
+ ...(definition.toolsNextCursor === undefined ? {} : { toolsNextCursor: definition.toolsNextCursor }),
1437
+ ...(definition.toolsHasMore === undefined ? {} : { toolsHasMore: definition.toolsHasMore }),
1438
+ ...(definition.featuredToolNames === undefined ? {} : { featuredToolNames: [...definition.featuredToolNames] }),
1439
+ };
1440
+ }
1441
+ /** @internal Package-local untrusted-cache boundary; not re-exported from the package root. */
1442
+ export function normalizePersistedConnectorDefinition(value) {
1443
+ if (!value || typeof value !== 'object' || Array.isArray(value))
1444
+ return undefined;
1445
+ const record = value;
1446
+ if (typeof record.id !== 'string' || typeof record.name !== 'string' || record.provider !== 'composio' || typeof record.category !== 'string')
1447
+ return undefined;
1448
+ if (record.authentication !== 'composio')
1449
+ return undefined;
1450
+ const tools = Array.isArray(record.tools)
1451
+ ? record.tools.map(normalizePersistedConnectorToolDefinition).filter((tool) => tool !== undefined)
1452
+ : [];
1453
+ const definition = {
1454
+ id: record.id,
1455
+ name: record.name,
1456
+ provider: record.provider,
1457
+ category: record.category,
1458
+ authentication: record.authentication,
1459
+ tools,
1460
+ allowedToolNames: [],
1461
+ };
1462
+ if (typeof record.description === 'string')
1463
+ definition.description = record.description;
1464
+ if (typeof record.providerConnectorId === 'string')
1465
+ definition.providerConnectorId = record.providerConnectorId;
1466
+ if (Array.isArray(record.curatedToolNames))
1467
+ definition.curatedToolNames = record.curatedToolNames.filter((item) => typeof item === 'string');
1468
+ if (Array.isArray(record.featuredToolNames))
1469
+ definition.featuredToolNames = record.featuredToolNames.filter((item) => typeof item === 'string');
1470
+ if (typeof record.toolCount === 'number' && Number.isFinite(record.toolCount) && record.toolCount >= 0) {
1471
+ definition.toolCount = record.toolCount;
1472
+ }
1473
+ if (record.minimumApproval === 'auto' || record.minimumApproval === 'confirm' || record.minimumApproval === 'disabled') {
1474
+ definition.minimumApproval = record.minimumApproval;
1475
+ }
1476
+ if (typeof record.disabled === 'boolean')
1477
+ definition.disabled = record.disabled;
1478
+ if (typeof record.toolsNextCursor === 'string')
1479
+ definition.toolsNextCursor = record.toolsNextCursor;
1480
+ if (typeof record.toolsHasMore === 'boolean')
1481
+ definition.toolsHasMore = record.toolsHasMore;
1482
+ const callableToolNames = new Set(tools
1483
+ .filter((tool) => tool.refreshEligible && tool.inputSchemaUnsupportedReason === undefined)
1484
+ .map((tool) => tool.name));
1485
+ definition.allowedToolNames = Array.isArray(record.allowedToolNames)
1486
+ ? record.allowedToolNames.filter((item) => typeof item === 'string' && callableToolNames.has(item))
1487
+ : [];
1488
+ return definition;
1489
+ }
1490
+ /** @internal Package-local untrusted-cache boundary; not re-exported from the package root. */
1491
+ export function normalizePersistedConnectorToolDefinition(value) {
1492
+ if (!value || typeof value !== 'object' || Array.isArray(value))
1493
+ return undefined;
1494
+ const record = value;
1495
+ if (typeof record.name !== 'string' || typeof record.title !== 'string')
1496
+ return undefined;
1497
+ const inputSchemaJson = toBoundedJsonObject(record.inputSchemaJson);
1498
+ const inputSchemaUnsupportedReason = inputSchemaJson === undefined
1499
+ ? 'persisted provider input schema is missing or invalid'
1500
+ : getConnectorSchemaSupportError(inputSchemaJson);
1501
+ return defineConnectorTool({
1502
+ name: record.name,
1503
+ title: record.title,
1504
+ ...(typeof record.description === 'string' ? { description: record.description } : {}),
1505
+ ...(inputSchemaJson === undefined ? {} : { inputSchemaJson }),
1506
+ ...(inputSchemaUnsupportedReason === undefined ? {} : { inputSchemaUnsupportedReason }),
1507
+ ...(toBoundedJsonObject(record.outputSchemaJson) === undefined ? {} : { outputSchemaJson: toBoundedJsonObject(record.outputSchemaJson) }),
1508
+ ...(record.curation && typeof record.curation === 'object' && !Array.isArray(record.curation)
1509
+ ? {
1510
+ curation: {
1511
+ ...((record.curation.useCases && Array.isArray(record.curation.useCases))
1512
+ ? { useCases: record.curation.useCases.filter((item) => typeof item === 'string' && item.trim().length > 0) }
1513
+ : {}),
1514
+ ...(typeof record.curation.reason === 'string' ? { reason: record.curation.reason } : {}),
1515
+ },
1516
+ }
1517
+ : {}),
1518
+ requiredScopes: Array.isArray(record.requiredScopes) ? record.requiredScopes.filter((item) => typeof item === 'string') : [],
1519
+ ...(typeof record.providerToolId === 'string' ? { providerToolId: record.providerToolId } : {}),
1520
+ });
1521
+ }
1522
+ /** @internal Package-local persistence contract; not re-exported from the package root. */
1523
+ export function readPersistedComposioCatalogCache(filePath) {
1524
+ try {
1525
+ const parsed = JSON.parse(readBoundedUtf8File(filePath, COMPOSIO_MAX_CACHE_BYTES));
1526
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
1527
+ return undefined;
1528
+ const record = parsed;
1529
+ if (record.schemaVersion !== 1 || record.provider !== 'composio' || typeof record.fetchedAt !== 'string' || !Array.isArray(record.definitions))
1530
+ return undefined;
1531
+ return {
1532
+ schemaVersion: 1,
1533
+ provider: 'composio',
1534
+ fetchedAt: record.fetchedAt,
1535
+ definitions: record.definitions.map(normalizePersistedConnectorDefinition).filter((definition) => definition !== undefined),
1536
+ };
1537
+ }
1538
+ catch (error) {
1539
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
1540
+ return undefined;
1541
+ return undefined;
1542
+ }
1543
+ }
1544
+ /**
1545
+ * Writes a bounded catalog cache atomically while holding its target lock.
1546
+ *
1547
+ * @internal Package-local persistence contract; not re-exported from the
1548
+ * package root.
1549
+ * @complexity Time: O(n). Space: O(n), capped by the cache byte limit.
1550
+ * @overallScore 98/100 — synchronous serialization is bounded and matches the
1551
+ * package's synchronous persistence adapters.
1552
+ */
1553
+ export function writePersistedComposioCatalogCache(filePath, cache) {
1554
+ withExclusiveFileLock(filePath, () => {
1555
+ const directory = path.dirname(filePath);
1556
+ fs.chmodSync(directory, 0o700);
1557
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
1558
+ const serialized = `${JSON.stringify(cache, null, 2)}\n`;
1559
+ if (Buffer.byteLength(serialized, 'utf8') > COMPOSIO_MAX_CACHE_BYTES) {
1560
+ throw new BoundedDataError(`Composio catalog cache exceeds the ${COMPOSIO_MAX_CACHE_BYTES}-byte limit`);
1561
+ }
1562
+ try {
1563
+ fs.writeFileSync(tempPath, serialized, { encoding: 'utf8', mode: 0o600 });
1564
+ fs.renameSync(tempPath, filePath);
1565
+ fs.chmodSync(filePath, 0o600);
1566
+ }
1567
+ catch (error) {
1568
+ try {
1569
+ fs.unlinkSync(tempPath);
1570
+ }
1571
+ catch (cleanupError) {
1572
+ if (!isErrno(cleanupError, 'ENOENT'))
1573
+ throw cleanupError;
1574
+ }
1575
+ throw error;
1576
+ }
1577
+ });
1578
+ }
1579
+ function getString(value) {
1580
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
1581
+ }
1582
+ /** @internal Package-local error-shape contract; not re-exported from the package root. */
1583
+ export function isErrno(error, code) {
1584
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === code);
1585
+ }
1586
+ function getNonNegativeInteger(value) {
1587
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
1588
+ }
1589
+ function getStringArray(value) {
1590
+ if (!Array.isArray(value))
1591
+ return [];
1592
+ return value.filter((item) => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim());
1593
+ }
1594
+ async function mapWithConcurrency(items, concurrency, mapper) {
1595
+ const results = new Array(items.length);
1596
+ let nextIndex = 0;
1597
+ const workerCount = Math.min(Math.max(1, concurrency), items.length);
1598
+ await Promise.all(Array.from({ length: workerCount }, async () => {
1599
+ while (nextIndex < items.length) {
1600
+ const index = nextIndex;
1601
+ nextIndex += 1;
1602
+ results[index] = await mapper(items[index], index);
1603
+ }
1604
+ }));
1605
+ return results;
1606
+ }
1607
+ /** @internal Package-local provider normalization contract; not re-exported from the package root. */
1608
+ export function getComposioToolkitDescription(toolkit) {
1609
+ const description = getString(toolkit?.meta?.description) ?? getString(toolkit?.description);
1610
+ if (!description || isGenericComposioDescription(description))
1611
+ return undefined;
1612
+ return description;
1613
+ }
1614
+ /** @internal Package-local provider normalization contract; not re-exported from the package root. */
1615
+ export function getComposioToolkitToolCount(toolkit) {
1616
+ return getNonNegativeInteger(toolkit?.meta?.tools_count) ?? getNonNegativeInteger(toolkit?.meta?.toolsCount);
1617
+ }
1618
+ /** @internal Package-local provider normalization contract; not re-exported from the package root. */
1619
+ export function isGenericComposioDescription(description) {
1620
+ return /^connect to .+ through composio\.?$/i.test(description.trim())
1621
+ || /^.+ integration via composio\.?$/i.test(description.trim());
1622
+ }
1623
+ /** @internal Package-local catalog-description contract; not re-exported from the package root. */
1624
+ export function fallbackComposioDescription(name, category, productName) {
1625
+ const normalizedCategory = category?.trim().toLowerCase();
1626
+ if (normalizedCategory?.includes('project'))
1627
+ return `Coordinate ${name} projects, tasks, and workflow data inside ${productName}.`;
1628
+ if (normalizedCategory?.includes('communication'))
1629
+ return `Bring ${name} conversations, channels, and collaboration context into ${productName}.`;
1630
+ if (normalizedCategory?.includes('documentation'))
1631
+ return `Search and reuse ${name} knowledge, pages, and documentation in ${productName}.`;
1632
+ if (normalizedCategory?.includes('storage'))
1633
+ return `Find and reference ${name} files, folders, and document metadata from ${productName}.`;
1634
+ if (normalizedCategory?.includes('developer'))
1635
+ return `Inspect ${name} developer resources, activity, and operational context from ${productName}.`;
1636
+ if (normalizedCategory?.includes('crm') || normalizedCategory?.includes('sales'))
1637
+ return `Use ${name} customer, deal, and account context in ${productName}.`;
1638
+ if (normalizedCategory?.includes('marketing'))
1639
+ return `Analyze ${name} campaigns, audiences, and marketing activity from ${productName}.`;
1640
+ if (normalizedCategory?.includes('finance') || normalizedCategory?.includes('commerce'))
1641
+ return `Work with ${name} business, billing, and transaction data in ${productName}.`;
1642
+ if (normalizedCategory?.includes('observability'))
1643
+ return `Surface ${name} incidents, metrics, and operational signals in ${productName}.`;
1644
+ if (normalizedCategory?.includes('data'))
1645
+ return `Query ${name} datasets and platform metadata for data-backed work in ${productName}.`;
1646
+ return `Use ${name} tools and data directly from ${productName}.`;
1647
+ }
1648
+ /** @internal Package-local provider wire contract; not re-exported from the package root. */
1649
+ export function getComposioAuthConfigId(response) {
1650
+ return getString(response.id) ?? getString(response.auth_config?.id);
1651
+ }
1652
+ /** @internal Package-local provider wire contract; not re-exported from the package root. */
1653
+ export function getComposioToolkitSlug(response) {
1654
+ return getString(response.toolkit?.slug) ?? getString(response.toolkit_slug) ?? getString(response.toolkitSlug);
1655
+ }
1656
+ /** @internal Package-local provider wire contract; not re-exported from the package root. */
1657
+ export function getComposioConnectionId(response) {
1658
+ return getString(response.connected_account_id) ?? getString(response.connectedAccountId) ?? getString(response.id) ?? getString(response.nanoid);
1659
+ }
1660
+ function appendOAuthStateToCallbackUrl(callbackUrl, state) {
1661
+ const url = new URL(callbackUrl);
1662
+ url.searchParams.set('state', state);
1663
+ return url.toString();
1664
+ }
1665
+ /** @internal Package-local provider normalization contract; not re-exported from the package root. */
1666
+ export function connectorIdForToolkitSlug(toolkitSlug) {
1667
+ const normalized = normalizeComposioSlug(toolkitSlug);
1668
+ if (normalized === 'googledrive' || normalized === 'gdrive' || normalized === 'drive')
1669
+ return 'google_drive';
1670
+ return normalized;
1671
+ }
1672
+ function normalizeComposioSlug(value) {
1673
+ return value.toLowerCase().replace(/[^a-z0-9]/g, '');
1674
+ }
1675
+ /** @internal Package-local provider normalization contract; not re-exported from the package root. */
1676
+ export function normalizeToolName(value) {
1677
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'tool';
1678
+ }
1679
+ function normalizeProviderToolId(value) {
1680
+ return normalizeToolName(value);
1681
+ }
1682
+ /** @internal Package-local curation contract; not re-exported from the package root. */
1683
+ export function applyComposioToolCuration(tool, connectorId, providerToolId, curationOverlay) {
1684
+ const connectorKey = normalizeComposioSlug(connectorId);
1685
+ const overlay = curationOverlay[connectorKey];
1686
+ const toolKey = providerToolId ? normalizeProviderToolId(providerToolId) : undefined;
1687
+ const curation = toolKey ? overlay?.[toolKey] : undefined;
1688
+ const safetyOverride = toolKey
1689
+ ? COMPOSIO_READ_ONLY_TOOL_SAFETY_OVERRIDES.has(`${connectorKey}:${toolKey}`)
1690
+ : false;
1691
+ const curated = curation === undefined
1692
+ ? tool
1693
+ : { ...tool, curation: { ...(tool.curation ?? {}), ...curation } };
1694
+ return safetyOverride && curated.safety.sideEffect === 'read' && curated.safety.approval === 'auto'
1695
+ ? {
1696
+ ...curated,
1697
+ safety: { ...COMPOSIO_READ_ONLY_TOOL_SAFETY },
1698
+ refreshEligible: true,
1699
+ }
1700
+ : curated;
1701
+ }
1702
+ function titleFromSlug(value) {
1703
+ return value
1704
+ .replace(/[_-]+/g, ' ')
1705
+ .trim()
1706
+ .replace(/\w\S*/g, (word) => `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`);
1707
+ }
1708
+ /** @internal Package-local provider normalization contract; not re-exported from the package root. */
1709
+ export function firstCategoryName(value) {
1710
+ if (!Array.isArray(value))
1711
+ return undefined;
1712
+ for (const item of value) {
1713
+ if (typeof item === 'string' && item.trim())
1714
+ return item.trim();
1715
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
1716
+ const record = item;
1717
+ const name = getString(record.name) ?? getString(record.slug);
1718
+ if (name)
1719
+ return name;
1720
+ }
1721
+ }
1722
+ return undefined;
1723
+ }
1724
+ function isCustomAuthRequiredMessage(message) {
1725
+ return /default auth config not found/i.test(message) || /does not have managed credentials/i.test(message);
1726
+ }
1727
+ /** @internal Package-local auth-failure classifier; not re-exported from the package root. */
1728
+ export function isComposioAuthenticationFailure(value) {
1729
+ const stack = [value];
1730
+ let inspected = 0;
1731
+ while (stack.length > 0 && inspected < 1_000) {
1732
+ inspected += 1;
1733
+ const current = stack.pop();
1734
+ if (current === 401)
1735
+ return true;
1736
+ if (typeof current === 'string') {
1737
+ const normalized = current.toLowerCase();
1738
+ if (normalized === '401'
1739
+ || /\bbad credentials\b/.test(normalized)
1740
+ || /\bunauthori[sz]ed\b/.test(normalized)
1741
+ || /\binvalid (?:access )?token\b/.test(normalized)
1742
+ || /\btoken (?:is )?expired\b/.test(normalized)
1743
+ || /\bexpired (?:access )?token\b/.test(normalized))
1744
+ return true;
1745
+ }
1746
+ else if (Array.isArray(current)) {
1747
+ stack.push(...current);
1748
+ }
1749
+ else if (current && typeof current === 'object') {
1750
+ stack.push(...Object.values(current));
1751
+ }
1752
+ }
1753
+ return false;
1754
+ }
1755
+ /** @internal Package-local provider error contract; not re-exported from the package root. */
1756
+ export function getCustomAuthRequiredMessage(error, definition) {
1757
+ if (error instanceof ConnectorServiceError && error.code === 'CONNECTOR_AUTH_CONFIG_REQUIRED')
1758
+ return error.message;
1759
+ const upstreamMessage = error instanceof Error ? error.message : String(error);
1760
+ if (!isCustomAuthRequiredMessage(upstreamMessage))
1761
+ return undefined;
1762
+ return `${definition.name} requires a custom Composio auth config. Create or enable a ${definition.name} auth config in Composio with your own OAuth credentials, then retry this connection.`;
1763
+ }
1764
+ /** @internal Package-local retry classifier; not re-exported from the package root. */
1765
+ export function isCachedAuthConfigRejection(error) {
1766
+ if (!(error instanceof ConnectorServiceError))
1767
+ return false;
1768
+ const httpStatus = error.details?.httpStatus;
1769
+ return httpStatus === 400 || httpStatus === 404;
1770
+ }
1771
+ /** @internal Package-local error-sanitization contract; not re-exported from the package root. */
1772
+ export async function getComposioSafeErrorMessage(response) {
1773
+ try {
1774
+ const payload = await readBoundedResponseJson(response, COMPOSIO_MAX_ERROR_RESPONSE_BYTES, 'Composio error response');
1775
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
1776
+ return undefined;
1777
+ const record = payload;
1778
+ const error = record.error && typeof record.error === 'object' && !Array.isArray(record.error)
1779
+ ? record.error
1780
+ : undefined;
1781
+ const candidate = getString(record.message)
1782
+ ?? getString(error?.message)
1783
+ ?? getString(record.error)
1784
+ ?? getString(record.detail)
1785
+ ?? getString(error?.suggested_fix);
1786
+ if (candidate && isCustomAuthRequiredMessage(candidate))
1787
+ return 'Default auth config not found';
1788
+ if (candidate && isComposioAuthenticationFailure(candidate))
1789
+ return 'Composio authentication failed';
1790
+ return undefined;
1791
+ }
1792
+ catch {
1793
+ return undefined;
1794
+ }
1795
+ }
1796
+ function isCanonicalJsonSchemaObject(schema) {
1797
+ return typeof schema.type === 'string'
1798
+ || Array.isArray(schema.type)
1799
+ || Array.isArray(schema.required)
1800
+ || schema.properties !== undefined
1801
+ || typeof schema.additionalProperties === 'boolean'
1802
+ || schema.allOf !== undefined
1803
+ || schema.anyOf !== undefined
1804
+ || schema.oneOf !== undefined
1805
+ || schema.not !== undefined
1806
+ || schema.items !== undefined
1807
+ || schema.enum !== undefined
1808
+ || schema.const !== undefined;
1809
+ }
1810
+ /**
1811
+ * Converts Composio's parameter-map wire shape into strict object JSON Schema.
1812
+ *
1813
+ * Per-field `required` is an adapter annotation, not a JSON Schema keyword at
1814
+ * that location. Every other annotation remains subject to the package's
1815
+ * fail-closed supported-keyword validator.
1816
+ *
1817
+ * @complexity Time: O(p), where p is the bounded parameter count. Space: O(p).
1818
+ * @overallScore 100/100
1819
+ */
1820
+ function decodeComposioInputParameters(value) {
1821
+ if (value === true)
1822
+ return { schema: {} };
1823
+ if (value === false)
1824
+ return { schema: { not: {} } };
1825
+ if (value === undefined) {
1826
+ return {
1827
+ schema: {},
1828
+ unsupportedReason: 'provider input schema is missing',
1829
+ };
1830
+ }
1831
+ const bounded = toBoundedJsonObject(value);
1832
+ if (bounded === undefined) {
1833
+ return {
1834
+ schema: {},
1835
+ unsupportedReason: 'provider input schema is not a JSON Schema object or boolean',
1836
+ };
1837
+ }
1838
+ if (Object.keys(bounded).length === 0 || isCanonicalJsonSchemaObject(bounded)) {
1839
+ const unsupportedReason = getConnectorSchemaSupportError(bounded);
1840
+ return {
1841
+ schema: bounded,
1842
+ ...(unsupportedReason === undefined ? {} : { unsupportedReason }),
1843
+ };
1844
+ }
1845
+ const properties = {};
1846
+ const required = [];
1847
+ for (const [parameterName, parameterValue] of Object.entries(bounded)) {
1848
+ if (!parameterValue || typeof parameterValue !== 'object' || Array.isArray(parameterValue)) {
1849
+ return {
1850
+ schema: {},
1851
+ unsupportedReason: `provider input parameter "${parameterName}" is not a schema object`,
1852
+ };
1853
+ }
1854
+ const parameterSchema = {};
1855
+ for (const [keyword, annotation] of Object.entries(parameterValue)) {
1856
+ if (keyword === 'required') {
1857
+ if (typeof annotation !== 'boolean') {
1858
+ return {
1859
+ schema: {},
1860
+ unsupportedReason: `provider input parameter "${parameterName}" has an invalid required annotation`,
1861
+ };
1862
+ }
1863
+ if (annotation)
1864
+ required.push(parameterName);
1865
+ continue;
1866
+ }
1867
+ Object.defineProperty(parameterSchema, keyword, {
1868
+ configurable: true,
1869
+ enumerable: true,
1870
+ value: annotation,
1871
+ writable: true,
1872
+ });
1873
+ }
1874
+ Object.defineProperty(properties, parameterName, {
1875
+ configurable: true,
1876
+ enumerable: true,
1877
+ value: parameterSchema,
1878
+ writable: true,
1879
+ });
1880
+ }
1881
+ const schema = {
1882
+ type: 'object',
1883
+ properties,
1884
+ required,
1885
+ additionalProperties: false,
1886
+ };
1887
+ const unsupportedReason = getConnectorSchemaSupportError(schema);
1888
+ return {
1889
+ schema,
1890
+ ...(unsupportedReason === undefined ? {} : { unsupportedReason }),
1891
+ };
1892
+ }
1893
+ function toBoundedJsonValue(value) {
1894
+ return toStructurallyBoundedJsonValue(value);
1895
+ }
1896
+ function toBoundedJsonObject(value) {
1897
+ if (value === undefined || value === null)
1898
+ return undefined;
1899
+ const bounded = toBoundedJsonValue(value);
1900
+ return bounded && typeof bounded === 'object' && !Array.isArray(bounded) ? bounded : undefined;
1901
+ }
1902
+ //# sourceMappingURL=composio.js.map