@jskit-ai/connectors-catalog 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (399) hide show
  1. package/README.md +192 -0
  2. package/docs/ai-upstream-notices.md +51 -0
  3. package/docs/ai.md +191 -0
  4. package/docs/airtable.md +130 -0
  5. package/docs/algolia.md +190 -0
  6. package/docs/amazon-redshift.md +229 -0
  7. package/docs/amplitude.md +210 -0
  8. package/docs/apify.md +162 -0
  9. package/docs/apollo-io.md +200 -0
  10. package/docs/asana.md +126 -0
  11. package/docs/ashby.md +188 -0
  12. package/docs/atlassian.md +224 -0
  13. package/docs/attention.md +162 -0
  14. package/docs/aws-athena.md +150 -0
  15. package/docs/aws-credentials.md +75 -0
  16. package/docs/aws-s3.md +193 -0
  17. package/docs/bigquery.md +146 -0
  18. package/docs/brevo.md +122 -0
  19. package/docs/calendly.md +158 -0
  20. package/docs/canva.md +191 -0
  21. package/docs/chargebee.md +192 -0
  22. package/docs/clay.md +194 -0
  23. package/docs/clickhouse.md +242 -0
  24. package/docs/confidence-exp.md +74 -0
  25. package/docs/confidence-flags.md +75 -0
  26. package/docs/confidence-oauth.md +137 -0
  27. package/docs/contentful.md +123 -0
  28. package/docs/databricks.md +180 -0
  29. package/docs/dbt-semantic-layer.md +175 -0
  30. package/docs/elevenlabs.md +102 -0
  31. package/docs/figma.md +132 -0
  32. package/docs/firebase-cloud-messaging.md +262 -0
  33. package/docs/firecrawl.md +137 -0
  34. package/docs/fireflies.md +95 -0
  35. package/docs/fireworks-ai.md +53 -0
  36. package/docs/gatewayapi.md +113 -0
  37. package/docs/gemini-enterprise.md +224 -0
  38. package/docs/github-api.md +133 -0
  39. package/docs/gitlab-api.md +138 -0
  40. package/docs/gmail.md +133 -0
  41. package/docs/gong.md +205 -0
  42. package/docs/google-ads.md +156 -0
  43. package/docs/google-analytics.md +202 -0
  44. package/docs/google-docs.md +105 -0
  45. package/docs/google-drive.md +108 -0
  46. package/docs/google-maps-platform.md +208 -0
  47. package/docs/google-oauth.md +123 -0
  48. package/docs/google-search-console.md +124 -0
  49. package/docs/google-sheets.md +94 -0
  50. package/docs/google-slides.md +95 -0
  51. package/docs/granola.md +178 -0
  52. package/docs/hex.md +161 -0
  53. package/docs/heygen.md +124 -0
  54. package/docs/hubspot.md +142 -0
  55. package/docs/incident-io.md +118 -0
  56. package/docs/inngest.md +178 -0
  57. package/docs/klipy.md +149 -0
  58. package/docs/lexware.md +144 -0
  59. package/docs/lightspeed.md +193 -0
  60. package/docs/linear.md +137 -0
  61. package/docs/linkedin.md +196 -0
  62. package/docs/logo-dev.md +152 -0
  63. package/docs/mailgun.md +153 -0
  64. package/docs/mapbox.md +174 -0
  65. package/docs/microsoft-excel.md +97 -0
  66. package/docs/microsoft-fabric.md +249 -0
  67. package/docs/microsoft-oauth.md +143 -0
  68. package/docs/microsoft-onedrive.md +76 -0
  69. package/docs/microsoft-onenote.md +75 -0
  70. package/docs/microsoft-outlook.md +90 -0
  71. package/docs/microsoft-powerpoint.md +119 -0
  72. package/docs/microsoft-sharepoint.md +75 -0
  73. package/docs/microsoft-teams.md +72 -0
  74. package/docs/microsoft-word.md +119 -0
  75. package/docs/miro.md +117 -0
  76. package/docs/n8n.md +169 -0
  77. package/docs/notion.md +137 -0
  78. package/docs/oura.md +161 -0
  79. package/docs/paddle.md +168 -0
  80. package/docs/perplexity.md +85 -0
  81. package/docs/pipedrive.md +120 -0
  82. package/docs/polar.md +63 -0
  83. package/docs/posthog.md +227 -0
  84. package/docs/prestashop.md +151 -0
  85. package/docs/replicate.md +60 -0
  86. package/docs/resend.md +153 -0
  87. package/docs/salesforce.md +234 -0
  88. package/docs/sanity.md +122 -0
  89. package/docs/semrush.md +276 -0
  90. package/docs/sentry.md +112 -0
  91. package/docs/sevdesk.md +107 -0
  92. package/docs/shopify.md +206 -0
  93. package/docs/slack.md +257 -0
  94. package/docs/snowflake.md +271 -0
  95. package/docs/storyblok.md +130 -0
  96. package/docs/stripe.md +77 -0
  97. package/docs/supabase.md +51 -0
  98. package/docs/tally.md +105 -0
  99. package/docs/telegram.md +165 -0
  100. package/docs/tiktok.md +201 -0
  101. package/docs/twilio.md +284 -0
  102. package/docs/twitch.md +428 -0
  103. package/docs/wave.md +440 -0
  104. package/docs/wix.md +1249 -0
  105. package/docs/wiz.md +177 -0
  106. package/docs/woocommerce.md +400 -0
  107. package/docs/wordpress-com.md +358 -0
  108. package/docs/wordpress-self-hosted.md +252 -0
  109. package/docs/workday.md +292 -0
  110. package/docs/x-twitter.md +188 -0
  111. package/docs/xero.md +429 -0
  112. package/docs/zoho-books.md +214 -0
  113. package/docs/zoho-crm.md +192 -0
  114. package/package.json +153 -0
  115. package/patterns/ai-connections/PATTERN.md +84 -0
  116. package/patterns/ai-connections/example/ai-model.js +11 -0
  117. package/patterns/ai-connections/example/perplexity-answer.js +29 -0
  118. package/patterns/api-key-connection/PATTERN.md +368 -0
  119. package/patterns/api-key-connection/example/connections.js +18 -0
  120. package/patterns/api-key-connection/example/integrations.json +19 -0
  121. package/patterns/assistant-mcp/PATTERN.md +110 -0
  122. package/patterns/assistant-mcp/example/integrations.json +13 -0
  123. package/patterns/assistant-mcp-oauth/PATTERN.md +235 -0
  124. package/patterns/assistant-mcp-oauth/example/integrations.json +102 -0
  125. package/patterns/aws-storage-queries/PATTERN.md +169 -0
  126. package/patterns/aws-storage-queries/example/formats/data-formats.js +35 -0
  127. package/patterns/aws-storage-queries/example/formats/package-lock.json +49 -0
  128. package/patterns/aws-storage-queries/example/formats/package.json +12 -0
  129. package/patterns/aws-storage-queries/example/formats/verify-formats.mjs +37 -0
  130. package/patterns/aws-storage-queries/example/integrations.json +38 -0
  131. package/patterns/aws-storage-queries/example/s3-transfer.js +34 -0
  132. package/patterns/event-delivery/PATTERN.md +151 -0
  133. package/patterns/event-delivery/example/integrations.json +17 -0
  134. package/patterns/firebase-messaging/PATTERN.md +156 -0
  135. package/patterns/firebase-messaging/example/server/notifications.js +23 -0
  136. package/patterns/google-ads-search/PATTERN.md +92 -0
  137. package/patterns/google-ads-search/example/ads-setup.js +25 -0
  138. package/patterns/oauth-connection/PATTERN.md +724 -0
  139. package/patterns/oauth-connection/example/integrations.json +20 -0
  140. package/patterns/paddle-catalogue/PATTERN.md +78 -0
  141. package/patterns/paddle-catalogue/example/create-products.js +38 -0
  142. package/patterns/public-image/PATTERN.md +84 -0
  143. package/patterns/public-image/example/integrations.json +13 -0
  144. package/patterns/public-image/example/logo-url.js +16 -0
  145. package/patterns/redshift-queries/PATTERN.md +150 -0
  146. package/patterns/redshift-queries/example/integrations.json +33 -0
  147. package/patterns/source-scanning/PATTERN.md +83 -0
  148. package/patterns/source-scanning/example/source-scanner.js +13 -0
  149. package/src/client/firebase-cloud-messaging.js +10 -0
  150. package/src/client/google-analytics.js +72 -0
  151. package/src/client/google-maps-platform.js +39 -0
  152. package/src/client/logo-dev.js +36 -0
  153. package/src/server/ai.js +46 -0
  154. package/src/server/airtable.js +67 -0
  155. package/src/server/algolia.js +63 -0
  156. package/src/server/amazon-redshift.js +96 -0
  157. package/src/server/amplitude.js +31 -0
  158. package/src/server/apify.js +68 -0
  159. package/src/server/apollo-io.js +85 -0
  160. package/src/server/asana.js +64 -0
  161. package/src/server/ashby.js +61 -0
  162. package/src/server/atlassian.js +32 -0
  163. package/src/server/attention.js +93 -0
  164. package/src/server/aws-athena.js +82 -0
  165. package/src/server/aws-s3.js +49 -0
  166. package/src/server/awsTransport.js +75 -0
  167. package/src/server/bigquery.js +97 -0
  168. package/src/server/brevo.js +97 -0
  169. package/src/server/calendly.js +77 -0
  170. package/src/server/canva.js +13 -0
  171. package/src/server/chargebee.js +88 -0
  172. package/src/server/clay.js +78 -0
  173. package/src/server/clickhouse.js +66 -0
  174. package/src/server/confidence.js +29 -0
  175. package/src/server/contentful.js +40 -0
  176. package/src/server/databricks.js +55 -0
  177. package/src/server/dbt-semantic-layer.js +95 -0
  178. package/src/server/elevenlabs.js +81 -0
  179. package/src/server/figma.js +24 -0
  180. package/src/server/firebase-cloud-messaging.js +111 -0
  181. package/src/server/firecrawl.js +70 -0
  182. package/src/server/fireflies.js +31 -0
  183. package/src/server/fireworks-ai.js +16 -0
  184. package/src/server/gatewayapi.js +48 -0
  185. package/src/server/gemini-enterprise.js +47 -0
  186. package/src/server/github-api.js +90 -0
  187. package/src/server/gitlab-api.js +67 -0
  188. package/src/server/gmail.js +60 -0
  189. package/src/server/gong.js +52 -0
  190. package/src/server/google-ads-search.js +134 -0
  191. package/src/server/google-ads.js +80 -0
  192. package/src/server/google-docs.js +36 -0
  193. package/src/server/google-drive.js +71 -0
  194. package/src/server/google-maps-platform.js +90 -0
  195. package/src/server/google-search-console.js +31 -0
  196. package/src/server/google-sheets.js +60 -0
  197. package/src/server/google-slides.js +36 -0
  198. package/src/server/google.js +61 -0
  199. package/src/server/granola.js +115 -0
  200. package/src/server/graphqlOperation.js +15 -0
  201. package/src/server/hex.js +29 -0
  202. package/src/server/heygen.js +54 -0
  203. package/src/server/hubspot.js +85 -0
  204. package/src/server/incident-io.js +68 -0
  205. package/src/server/inngest.js +86 -0
  206. package/src/server/jsonOperation.js +34 -0
  207. package/src/server/klipy.js +34 -0
  208. package/src/server/lexware.js +59 -0
  209. package/src/server/lightspeed.js +82 -0
  210. package/src/server/linear.js +52 -0
  211. package/src/server/linkedin.js +74 -0
  212. package/src/server/mailgun.js +76 -0
  213. package/src/server/mapbox.js +46 -0
  214. package/src/server/mcpRegistration.js +41 -0
  215. package/src/server/mcpTransport.js +60 -0
  216. package/src/server/microsoft-excel.js +67 -0
  217. package/src/server/microsoft-fabric.js +81 -0
  218. package/src/server/microsoft-onedrive.js +58 -0
  219. package/src/server/microsoft-onenote.js +52 -0
  220. package/src/server/microsoft-outlook.js +58 -0
  221. package/src/server/microsoft-powerpoint.js +5 -0
  222. package/src/server/microsoft-sharepoint.js +46 -0
  223. package/src/server/microsoft-teams.js +29 -0
  224. package/src/server/microsoft-word.js +5 -0
  225. package/src/server/microsoft.js +88 -0
  226. package/src/server/miro.js +29 -0
  227. package/src/server/n8n.js +78 -0
  228. package/src/server/notion.js +76 -0
  229. package/src/server/oura.js +48 -0
  230. package/src/server/paddle.js +83 -0
  231. package/src/server/perplexity.js +15 -0
  232. package/src/server/pipedrive.js +95 -0
  233. package/src/server/polar.js +20 -0
  234. package/src/server/posthog.js +44 -0
  235. package/src/server/prestashop.js +42 -0
  236. package/src/server/replicate.js +42 -0
  237. package/src/server/resend.js +93 -0
  238. package/src/server/salesforce.js +84 -0
  239. package/src/server/sanity.js +31 -0
  240. package/src/server/semrush.js +359 -0
  241. package/src/server/sentry.js +30 -0
  242. package/src/server/sevdesk.js +20 -0
  243. package/src/server/shopify.js +99 -0
  244. package/src/server/slack.js +143 -0
  245. package/src/server/snowflake.js +139 -0
  246. package/src/server/storyblok.js +49 -0
  247. package/src/server/stripe.js +13 -0
  248. package/src/server/supabase.js +13 -0
  249. package/src/server/tally.js +78 -0
  250. package/src/server/telegram.js +55 -0
  251. package/src/server/tiktok.js +86 -0
  252. package/src/server/twilio.js +90 -0
  253. package/src/server/twitch.js +409 -0
  254. package/src/server/wave.js +520 -0
  255. package/src/server/wix.js +1048 -0
  256. package/src/server/wiz.js +117 -0
  257. package/src/server/woocommerce.js +412 -0
  258. package/src/server/wordpress-com.js +236 -0
  259. package/src/server/wordpress-self-hosted.js +134 -0
  260. package/src/server/workday.js +136 -0
  261. package/src/server/x-twitter.js +110 -0
  262. package/src/server/xero.js +397 -0
  263. package/src/server/zoho-books.js +90 -0
  264. package/src/server/zoho-crm.js +89 -0
  265. package/src/server/zohoOAuth.js +21 -0
  266. package/src/shared/ai-models.json +8049 -0
  267. package/src/shared/ai.js +63 -0
  268. package/src/shared/algolia.js +29 -0
  269. package/src/shared/amazon-redshift.js +51 -0
  270. package/src/shared/amplitude.js +28 -0
  271. package/src/shared/atlassian.js +43 -0
  272. package/src/shared/aws.js +72 -0
  273. package/src/shared/canva.js +66 -0
  274. package/src/shared/chargebee.js +26 -0
  275. package/src/shared/clickhouse.js +38 -0
  276. package/src/shared/confidence.js +28 -0
  277. package/src/shared/databricks.js +33 -0
  278. package/src/shared/dbt-semantic-layer.js +32 -0
  279. package/src/shared/definitions.js +140 -0
  280. package/src/shared/figma.js +21 -0
  281. package/src/shared/firebase-cloud-messaging.js +45 -0
  282. package/src/shared/gemini-enterprise.js +33 -0
  283. package/src/shared/gong.js +33 -0
  284. package/src/shared/google-ads-search.js +31 -0
  285. package/src/shared/google-ads.js +28 -0
  286. package/src/shared/google-maps-platform.js +28 -0
  287. package/src/shared/google.js +133 -0
  288. package/src/shared/granola.js +25 -0
  289. package/src/shared/hex.js +28 -0
  290. package/src/shared/inngest.js +35 -0
  291. package/src/shared/lightspeed.js +75 -0
  292. package/src/shared/linkedin.js +27 -0
  293. package/src/shared/logo-dev.js +21 -0
  294. package/src/shared/mapbox.js +38 -0
  295. package/src/shared/mcp.js +92 -0
  296. package/src/shared/microsoft-fabric.js +47 -0
  297. package/src/shared/microsoft.js +231 -0
  298. package/src/shared/miro.js +27 -0
  299. package/src/shared/oura.js +23 -0
  300. package/src/shared/posthog.js +33 -0
  301. package/src/shared/prestashop.js +23 -0
  302. package/src/shared/salesforce.js +36 -0
  303. package/src/shared/semrush.js +24 -0
  304. package/src/shared/shopify.js +51 -0
  305. package/src/shared/siteUrl.js +12 -0
  306. package/src/shared/slack.js +88 -0
  307. package/src/shared/snowflake.js +55 -0
  308. package/src/shared/tiktok.js +22 -0
  309. package/src/shared/tokens.js +747 -0
  310. package/src/shared/twilio.js +41 -0
  311. package/src/shared/twitch.js +49 -0
  312. package/src/shared/wave.js +52 -0
  313. package/src/shared/wix.js +40 -0
  314. package/src/shared/wiz.js +41 -0
  315. package/src/shared/wordpress-com.js +30 -0
  316. package/src/shared/wordpress.js +69 -0
  317. package/src/shared/workday.js +50 -0
  318. package/src/shared/x-twitter.js +19 -0
  319. package/src/shared/xero.js +46 -0
  320. package/src/shared/zoho-books.js +39 -0
  321. package/src/shared/zoho-crm.js +43 -0
  322. package/src/shared/zoho-regions.js +12 -0
  323. package/test/ai.test.js +136 -0
  324. package/test/airtable.test.js +125 -0
  325. package/test/algolia.test.js +207 -0
  326. package/test/amazon-redshift.test.js +272 -0
  327. package/test/amplitude.test.js +239 -0
  328. package/test/apify.test.js +93 -0
  329. package/test/apollo-io.test.js +125 -0
  330. package/test/asana.test.js +99 -0
  331. package/test/ashby.test.js +97 -0
  332. package/test/atlassian.test.js +249 -0
  333. package/test/attention.test.js +79 -0
  334. package/test/aws.test.js +405 -0
  335. package/test/brevo.test.js +71 -0
  336. package/test/calendly.test.js +136 -0
  337. package/test/canva.test.js +258 -0
  338. package/test/chargebee.test.js +195 -0
  339. package/test/clickhouse.test.js +278 -0
  340. package/test/confidence.test.js +205 -0
  341. package/test/databricks.test.js +253 -0
  342. package/test/dbt-semantic-layer.test.js +221 -0
  343. package/test/designMcp.test.js +336 -0
  344. package/test/elevenlabs.test.js +52 -0
  345. package/test/firebase-cloud-messaging.test.js +242 -0
  346. package/test/firecrawl.test.js +66 -0
  347. package/test/gemini-enterprise.test.js +218 -0
  348. package/test/github-api.test.js +167 -0
  349. package/test/gitlab-api.test.js +160 -0
  350. package/test/gong.test.js +192 -0
  351. package/test/google-ads.test.js +350 -0
  352. package/test/google-maps-platform.test.js +191 -0
  353. package/test/google.test.js +710 -0
  354. package/test/granola.test.js +240 -0
  355. package/test/hex.test.js +253 -0
  356. package/test/heygen.test.js +121 -0
  357. package/test/hubspot.test.js +103 -0
  358. package/test/inngest.test.js +219 -0
  359. package/test/lightspeed.test.js +250 -0
  360. package/test/linear.test.js +146 -0
  361. package/test/linkedin.test.js +243 -0
  362. package/test/logo-dev.test.js +79 -0
  363. package/test/mailgun.test.js +107 -0
  364. package/test/mapbox.test.js +181 -0
  365. package/test/mcp.test.js +227 -0
  366. package/test/microsoft-fabric.test.js +258 -0
  367. package/test/microsoft.test.js +554 -0
  368. package/test/n8nOAuth.test.js +188 -0
  369. package/test/notionOAuth.test.js +183 -0
  370. package/test/oura.test.js +211 -0
  371. package/test/paddleCatalogue.test.js +51 -0
  372. package/test/paddlePermissions.test.js +101 -0
  373. package/test/perplexityComposition.test.js +39 -0
  374. package/test/pipedriveOAuth.test.js +140 -0
  375. package/test/posthog.test.js +200 -0
  376. package/test/prestashop.test.js +207 -0
  377. package/test/resend.test.js +163 -0
  378. package/test/salesforce.test.js +276 -0
  379. package/test/semrush.test.js +479 -0
  380. package/test/sentry.test.js +35 -0
  381. package/test/shopify.test.js +247 -0
  382. package/test/slack.test.js +320 -0
  383. package/test/snowflake.test.js +391 -0
  384. package/test/tally.test.js +75 -0
  385. package/test/telegram.test.js +58 -0
  386. package/test/tiktok.test.js +243 -0
  387. package/test/tokens.test.js +1079 -0
  388. package/test/twilio.test.js +221 -0
  389. package/test/twitch.test.js +573 -0
  390. package/test/wave.test.js +678 -0
  391. package/test/wix.test.js +1099 -0
  392. package/test/wiz.test.js +185 -0
  393. package/test/wordpress-com.test.js +469 -0
  394. package/test/wordpress.test.js +660 -0
  395. package/test/workday.test.js +314 -0
  396. package/test/x-twitter.test.js +256 -0
  397. package/test/xero.test.js +700 -0
  398. package/test/zoho-books.test.js +253 -0
  399. package/test/zoho-crm.test.js +223 -0
@@ -0,0 +1,1079 @@
1
+ import assert from "node:assert/strict";
2
+ import { createHmac } from "node:crypto";
3
+ import test from "node:test";
4
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
8
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
9
+ import { airtableProvider } from "../src/server/airtable.js";
10
+ import { notionProvider } from "../src/server/notion.js";
11
+ import { brevoProvider } from "../src/server/brevo.js";
12
+ import { elevenlabsProvider } from "../src/server/elevenlabs.js";
13
+ import { githubApiProvider } from "../src/server/github-api.js";
14
+ import { apifyProvider } from "../src/server/apify.js";
15
+ import { calendlyProvider } from "../src/server/calendly.js";
16
+ import { hubspotProvider } from "../src/server/hubspot.js";
17
+ import { linearProvider } from "../src/server/linear.js";
18
+ import { pipedriveProvider } from "../src/server/pipedrive.js";
19
+ import { gitlabApiProvider } from "../src/server/gitlab-api.js";
20
+ import { tallyProvider } from "../src/server/tally.js";
21
+ import { contentfulProvider } from "../src/server/contentful.js";
22
+ import { asanaProvider } from "../src/server/asana.js";
23
+ import { ashbyProvider } from "../src/server/ashby.js";
24
+ import { lexwareProvider } from "../src/server/lexware.js";
25
+ import { sevdeskProvider } from "../src/server/sevdesk.js";
26
+ import { apolloIoProvider } from "../src/server/apollo-io.js";
27
+ import { attentionProvider } from "../src/server/attention.js";
28
+ import { telegramProvider } from "../src/server/telegram.js";
29
+ import { klipyProvider } from "../src/server/klipy.js";
30
+ import { clayProvider } from "../src/server/clay.js";
31
+
32
+ import { stripeProvider } from "../src/server/stripe.js";
33
+ import { replicateProvider } from "../src/server/replicate.js";
34
+ import { incidentIoProvider } from "../src/server/incident-io.js";
35
+ import { firefliesProvider } from "../src/server/fireflies.js";
36
+
37
+ import { heygenProvider } from "../src/server/heygen.js";
38
+ import { perplexityProvider } from "../src/server/perplexity.js";
39
+ import { supabaseProvider } from "../src/server/supabase.js";
40
+ import { paddleProvider } from "../src/server/paddle.js";
41
+ import { mailgunProvider } from "../src/server/mailgun.js";
42
+ import { fireworksAiProvider } from "../src/server/fireworks-ai.js";
43
+ import { gatewayApiProvider, verifyGatewayApiEvent } from "../src/server/gatewayapi.js";
44
+ import { polarProvider } from "../src/server/polar.js";
45
+ import { storyblokProvider } from "../src/server/storyblok.js";
46
+
47
+ const cases = [
48
+ { provider: clayProvider, check: "/public/v0/me", header: "clay-api-key", prefix: "",
49
+ response: { user: { id: "user-1", name: null }, workspace: { id: "workspace-1", name: "Fixture" } },
50
+ operation: "searches.create", operationPath: "/public/v0/search/query-mode",
51
+ input: { query: 'select from companies where people.exists(is_current = true and job_title is_similar_to ("VP Sales"))' }, body: { query: 'select from companies where people.exists(is_current = true and job_title is_similar_to ("VP Sales"))' },
52
+ operationResponse: { search_id: "search-1", source_type: "companies" } },
53
+ { provider: telegramProvider, check: "/getMe", key: "123456:fixture_token", rotatedKey: "123456:rotated_token",
54
+ pathPrefix: "/bot123456:fixture_token", rotatedPathPrefix: "/bot123456:rotated_token",
55
+ response: { ok: true, result: { id: 123456, is_bot: true, first_name: "Fixture bot", username: "fixture_bot" } },
56
+ operation: "webhook.read", operationPath: "/getWebhookInfo",
57
+ operationResponse: { ok: true, result: { url: "https://example.test/bot", has_custom_certificate: false, pending_update_count: 12 } } },
58
+ { provider: klipyProvider, check: "/clips/trending", pathPrefix: "/api/v1/private-fixture-key", rotatedPathPrefix: "/api/v1/rotated-fixture-key",
59
+ response: { result: true, data: { data: [{ slug: "fixture-clip", title: "Fixture", file: {} }], next: true } },
60
+ operation: "clips.search", operationPath: "/clips/search",
61
+ input: { q: "Cats & dogs", page: 2, per_page: 8, locale: "us", customer_id: "subject & 2", content_filter: "medium" },
62
+ query: { q: "Cats & dogs", page: "2", per_page: "8", locale: "us", customer_id: "subject & 2", content_filter: "medium" } },
63
+ { provider: apolloIoProvider, check: "/api/v1/accounts/search", header: "x-api-key", prefix: "", method: "POST",
64
+ response: { accounts: [{ id: "account-1", name: "Saved company" }] },
65
+ input: { page: 2, per_page: 10, q_organization_name: "Design & partners" }, body: { page: 2, per_page: 10, q_organization_name: "Design & partners" } },
66
+ { provider: attentionProvider, check: "/v2/conversations/list", header: "authorization", prefix: "Bearer ",
67
+ response: { data: [{ id: "conversation-1", type: "conversations", attributes: { title: "Review" } }], meta: { totalRecords: 150 }, links: { related: "/v2/conversations?page=2" } },
68
+ input: { page: 2, size: 10, "filter[title]": "Design & review", "filter[hide_internal]": false, detailedTranscript: true },
69
+ query: { page: "2", size: "10", "filter[title]": "Design & review", "filter[hide_internal]": "false", detailedTranscript: "true" } },
70
+ { provider: ashbyProvider, check: "/job.list", header: "authorization", method: "POST",
71
+ initialHeader: `Basic ${Buffer.from("private-fixture-key:").toString("base64")}`, rotatedHeader: `Basic ${Buffer.from("rotated-fixture-key:").toString("base64")}`,
72
+ response: { success: true, results: [{ id: "job-1", title: "Developer" }], moreDataAvailable: true, nextCursor: "next-cursor", syncToken: "current-sync" },
73
+ input: { limit: 2, cursor: "cursor & 2", syncToken: "sync & 1" }, body: { limit: 2, cursor: "cursor & 2", syncToken: "sync & 1" }, extraHeaders: { Accept: "application/json; version=1" } },
74
+ { provider: lexwareProvider, check: "/v1/contacts", header: "authorization", prefix: "Bearer ",
75
+ response: { content: [{ id: "contact-1", roles: { customer: { number: 1 } } }], first: true, last: false, totalPages: 2, totalElements: 26, numberOfElements: 1, size: 25, number: 0 },
76
+ input: { size: 2, page: 1, customer: false, vendor: true }, query: { size: "2", page: "1", customer: "false", vendor: "true" } },
77
+ { provider: sevdeskProvider, check: "/api/v1/Contact", header: "authorization", prefix: "",
78
+ response: { objects: [{ id: "1", objectName: "Contact", name: "Fixture" }], total: "101" },
79
+ input: { limit: 2, offset: 4, countAll: true, depth: 0 }, query: { limit: "2", offset: "4", countAll: "true", depth: "0" } },
80
+ { provider: storyblokProvider, check: "/v2/cdn/spaces/me", queryCredential: "token", response: { space: { id: 123, name: "Fixture", language_codes: ["de"], version: 1544117388 } }, operation: "stories.list", operationPath: "/v2/cdn/stories", operationResponse: { stories: [{ id: 456, content: { title: "Fixture" } }], cv: 1544117388, rels: [], links: [] }, input: { page: 2, per_page: 10, starts_with: "blog/", search_term: "Notes & plans", language: "de", cv: 1544117388 }, query: { version: "published", page: "2", per_page: "10", starts_with: "blog/", search_term: "Notes & plans", language: "de", cv: "1544117388" } },
81
+ { provider: fireworksAiProvider, check: "/v1/accounts", header: "authorization", prefix: "Bearer ", extraHeaders: { "content-type": "application/json" }, response: { accounts: [{ name: "accounts/fixture", displayName: "Fixture" }], nextPageToken: "next & 2", totalSize: 3 }, input: { pageSize: 2, pageToken: "next & 2", filter: "displayName=Fixture" }, query: { pageSize: "2", pageToken: "next & 2", filter: "displayName=Fixture" } },
82
+ { provider: gatewayApiProvider, check: "/rest/me", header: "authorization", prefix: "Token ", response: { id: 123, credit: "1234.56", currency: "DKK" } },
83
+ { provider: polarProvider, check: "/v1/products/", header: "authorization", prefix: "Bearer ", response: { items: [{ id: "product-1", name: "Fixture" }], pagination: { total_count: 21, max_page: 3 } }, input: { page: 2, limit: 10, query: "Fixture & plans", is_archived: false, is_recurring: true }, query: { page: "2", limit: "10", query: "Fixture & plans", is_archived: "false", is_recurring: "true" } },
84
+ { provider: paddleProvider, check: "/products", header: "authorization", prefix: "Bearer ", extraHeaders: { "paddle-version": "1" }, response: { data: [{ id: "pro_fixture", name: "Fixture" }], meta: { request_id: "request-1", pagination: { per_page: 50, has_more: true, next: "https://sandbox-api.paddle.com/products?after=pro_fixture" } } }, input: { per_page: 2, after: "pro_fixture", status: "archived" }, query: { per_page: "2", after: "pro_fixture", status: "archived" } },
85
+ { provider: mailgunProvider, check: "/v4/domains", header: "authorization", prefix: "Basic ", initialHeader: "Basic YXBpOnByaXZhdGUtZml4dHVyZS1rZXk=", rotatedHeader: "Basic YXBpOnJvdGF0ZWQtZml4dHVyZS1rZXk=", response: { total_count: 1, items: [{ name: "example.com", state: "active" }] }, input: { limit: 2, skip: 4, state: "active", sort: "name:asc", search: "example.com", include_subaccounts: true }, query: { limit: "2", skip: "4", state: "active", sort: "name:asc", search: "example.com", include_subaccounts: "true" } },
86
+ { provider: heygenProvider, check: "/v3/users/me", header: "x-api-key", prefix: "", response: { data: { username: "fixture", email: "fixture@example.com", billing_type: "wallet" } }, operation: "voices.list", operationPath: "/v3/voices", operationResponse: { data: [{ voice_id: "voice-1", name: "Fixture" }], has_more: true, next_token: "page & 2" }, input: { limit: 2, token: "page & 2", type: "private", engine: "starfish", language: "English", gender: "female" }, query: { limit: "2", token: "page & 2", type: "private", engine: "starfish", language: "English", gender: "female" } },
87
+ { provider: perplexityProvider, check: "/v1/async/sonar", header: "authorization", prefix: "Bearer ", response: { requests: [{ id: "request-1", created_at: 123, model: "sonar", status: "COMPLETED" }], next_token: null }, operation: "models.list", operationPath: "/v1/models", operationResponse: { object: "list", data: [{ id: "example/model", object: "model", created: 0, owned_by: "example" }] } },
88
+ { provider: supabaseProvider, check: "/v1/projects", header: "authorization", prefix: "Bearer ", response: [{ id: "project-1", ref: "project-reference", name: "Fixture", organization_id: "organization-1", region: "ap-southeast-2", status: "ACTIVE_HEALTHY" }] },
89
+ { provider: stripeProvider, check: "/v1/balance", header: "authorization", prefix: "Bearer ", response: { object: "balance", available: [{ amount: 1500, currency: "aud", source_types: { card: 1500 } }], pending: [], livemode: false } },
90
+ { provider: replicateProvider, check: "/v1/account", header: "authorization", prefix: "Bearer ", response: { type: "organization", username: "fixture", name: "Fixture" }, operation: "hardware.list", operationPath: "/v1/hardware", operationResponse: [{ name: "CPU", sku: "cpu" }] },
91
+ { provider: incidentIoProvider, check: "/v2/incidents", header: "authorization", prefix: "Bearer ", response: { incidents: [{ id: "incident-1", name: "Fixture" }], pagination_meta: { page_size: 25, after: "next", total_record_count: 42 } }, input: { page_size: 2, after: "next & 2", sort_by: "created_at_oldest_first" }, query: { page_size: "2", after: "next & 2", sort_by: "created_at_oldest_first" } },
92
+ { provider: firefliesProvider, check: "/graphql", header: "authorization", prefix: "Bearer ", method: "POST", response: { data: { user: { user_id: "user-1", name: "Fixture", email: "test@example.com" } } }, operation: "transcripts.list", operationResponse: { data: { transcripts: [{ id: "meeting-1", title: "Fixture" }] } }, input: { limit: 2, skip: 4, mine: false }, body: { query: "query ConnectorTranscripts($limit: Int!, $skip: Int!, $mine: Boolean!) { transcripts(limit: $limit, skip: $skip, mine: $mine) { id title } }", variables: { limit: 2, skip: 4, mine: false } } },
93
+ { provider: airtableProvider, check: "/v0/meta/bases", header: "authorization", prefix: "Bearer ", response: { bases: [{ id: "app1", name: "Example" }], offset: "next/page" }, input: { offset: "next/page & 2" }, query: { offset: "next/page & 2" } },
94
+ { provider: notionProvider, check: "/v1/search", header: "authorization", prefix: "Bearer ", response: { object: "list", results: [], next_cursor: "next", has_more: true }, method: "POST", input: { query: "Notes & plans", start_cursor: "cursor", page_size: 2 }, body: { query: "Notes & plans", start_cursor: "cursor", page_size: 2 }, extraHeaders: { "Notion-Version": "2026-03-11" } },
95
+ { provider: brevoProvider, check: "/v3/contacts", header: "api-key", prefix: "", response: { contacts: [], count: 12 }, input: { limit: 2, offset: 4, sort: "asc" }, query: { limit: "2", offset: "4", sort: "asc" } },
96
+ { provider: elevenlabsProvider, check: "/v1/user", header: "xi-api-key", prefix: "", response: { user_id: "user-1" }, operation: "voices.list", operationPath: "/v2/voices", operationResponse: { voices: [], next_page_token: "page", has_more: true }, input: { page_size: 2, next_page_token: "page & 2", search: "Voice" }, query: { page_size: "2", next_page_token: "page & 2", search: "Voice" } },
97
+ { provider: githubApiProvider, check: "/user", header: "authorization", prefix: "Bearer ", response: { id: 42, login: "example" }, operation: "repositories.list", operationPath: "/user/repos", operationResponse: [{ id: 10, full_name: "example/repository" }], input: { per_page: 2, page: 3 }, query: { per_page: "2", page: "3" }, extraHeaders: { "X-GitHub-Api-Version": "2026-03-10", "User-Agent": "jskit-connectors", Accept: "application/vnd.github+json" } },
98
+ { provider: apifyProvider, check: "/v2/actors", header: "authorization", prefix: "Bearer ", response: { data: { items: [], total: 12, offset: 0, count: 0, limit: 100 } }, input: { limit: 2, offset: 4, desc: true }, query: { limit: "2", offset: "4", desc: "true" } },
99
+ { provider: calendlyProvider, check: "/users/me", header: "authorization", prefix: "Bearer ", response: { resource: { uri: "https://api.calendly.com/users/fixture", name: "Fixture" } }, operation: "eventTypes.list", operationPath: "/event_types", operationResponse: { collection: [], pagination: { count: 0, next_page_token: "page & 2" } }, input: { user: "https://api.calendly.com/users/fixture", count: 2, page_token: "page & 2", active: false }, query: { user: "https://api.calendly.com/users/fixture", count: "2", page_token: "page & 2", active: "false" } },
100
+ { provider: hubspotProvider, scopes: ["crm.objects.contacts.read"], check: "/crm/objects/2026-09/contacts", header: "authorization", prefix: "Bearer ", response: { results: [], paging: { next: { after: "42" } } }, input: { limit: 2, after: "42", archived: true }, query: { limit: "2", after: "42", archived: "true" } },
101
+ { provider: linearProvider, scopes: ["read"], check: "/graphql", header: "authorization", prefix: "", response: { data: { viewer: { id: "user-1", name: "Fixture" } } }, method: "POST", operation: "issues.list", operationResponse: { data: { issues: { nodes: [], pageInfo: { hasNextPage: true, endCursor: "next" } } } }, input: { first: 2, after: "cursor & 2" }, body: { query: "query ConnectorIssues($first: Int!, $after: String) { issues(first: $first, after: $after) { nodes { id identifier title } pageInfo { hasNextPage endCursor } } }", variables: { first: 2, after: "cursor & 2" } } },
102
+ { provider: pipedriveProvider, check: "/v1/users/me", header: "x-api-token", prefix: "", response: { success: true, data: { id: 42, name: "Fixture", company_domain: "fixture" } } },
103
+ { provider: gitlabApiProvider, check: "/api/v4/user", header: "PRIVATE-TOKEN", prefix: "", response: { id: 42, username: "fixture" }, operation: "projects.list", operationPath: "/api/v4/projects", operationResponse: [{ id: 1, path_with_namespace: "fixture/project" }], input: { per_page: 2, page: 3 }, query: { per_page: "2", page: "3", membership: "true", simple: "true" } },
104
+ { provider: tallyProvider, check: "/forms", header: "authorization", prefix: "Bearer ", response: { items: [], page: 1, limit: 50, total: 0, hasMore: false }, input: { limit: 2, page: 3 }, query: { limit: "2", page: "3" }, extraHeaders: { "tally-version": "2025-02-01" } },
105
+ { provider: contentfulProvider, settings: { spaceId: "space-one" }, check: "/spaces/space-one/environments/master/entries", header: "authorization", prefix: "Bearer ", response: { sys: { type: "Array" }, items: [], skip: 0, limit: 1, total: 0 }, input: { limit: 2, skip: 4, content_type: "article" }, query: { limit: "2", skip: "4", content_type: "article" } },
106
+ { provider: asanaProvider, check: "/api/1.0/workspaces", header: "authorization", prefix: "Bearer ", response: { data: [{ gid: "123", name: "Fixture" }], next_page: { offset: "next & 2" } }, input: { limit: 2, offset: "next & 2" }, query: { limit: "2", offset: "next & 2" } }
107
+ ];
108
+
109
+ async function fixture(t, entry) {
110
+ const directory = await mkdtemp(path.join(tmpdir(), "connector-token-"));
111
+ t.after(() => rm(directory, { recursive: true, force: true }));
112
+ const state = { key: entry.key || "private-fixture-key", status: 200, response: entry.response };
113
+ const requests = [];
114
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(5) }, activeKeyId: "current" });
115
+ const options = {
116
+ configuration: { schemaVersion: 1, registrations: {}, integrations: {
117
+ source: { provider: entry.provider.id, ...(entry.settings ? { settings: entry.settings } : {}), accountMode: "shared", scopes: entry.scopes || (entry.provider.id === "calendly" ? ["users:read", "event_types:read"] : []), authentication: { method: "api-key", secretRef: "env:SERVICE_TOKEN" } }
118
+ } },
119
+ providers: [entry.provider], authorize: async (context) => context,
120
+ resolveReference: async () => state.key,
121
+ store: createFileConnectionStore({ directory, protection }),
122
+ fetchImpl: async (url, init) => {
123
+ requests.push({ url: new URL(String(url)), init, headers: new Headers(init.headers) });
124
+ return Response.json(state.response, { status: state.status });
125
+ }
126
+ };
127
+ const input = { context: { applicationId: "app-one", subjectId: "account-one" }, integrationId: "source" };
128
+ return { service: createConnectionService(options), options, directory, protection, input, state, requests };
129
+ }
130
+
131
+ for (const entry of cases) {
132
+ test(`${entry.provider.id}: verifies its credential and performs the documented operation after a file-store restart`, async (t) => {
133
+ const { service, options, directory, protection, input, state, requests } = await fixture(t, entry);
134
+ assert.equal((await service.connectApiKey(input)).status, "connected");
135
+ const first = requests[0];
136
+ assert.equal(first.url.origin, (typeof entry.provider.apiOrigins === "function" ? entry.provider.apiOrigins(entry.settings || {}) : entry.provider.apiOrigins)[0]);
137
+ assert.equal(first.url.pathname, (entry.pathPrefix || "") + entry.check);
138
+ assert.equal(first.init.method, entry.method || "GET");
139
+ if (entry.pathPrefix) {
140
+ assert.equal(first.headers.has("authorization"), false);
141
+ } else if (entry.queryCredential) {
142
+ assert.deepEqual(first.url.searchParams.getAll(entry.queryCredential), [state.key]);
143
+ assert.equal(first.headers.has("authorization"), false);
144
+ } else assert.equal(first.headers.get(entry.header), entry.initialHeader || entry.prefix + state.key);
145
+ assert.equal(first.init.redirect, "error");
146
+ for (const [header, value] of Object.entries(entry.extraHeaders || {})) assert.equal(first.headers.get(header), value);
147
+ for (const name of await readdir(directory)) assert.equal((await readFile(path.join(directory, name), "utf8")).includes(state.key), false);
148
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
149
+ assert.equal((await restarted.status(input)).status, "connected");
150
+ state.response = entry.operationResponse || entry.response;
151
+ state.key = entry.rotatedKey || "rotated-fixture-key";
152
+ const result = await restarted.invoke({ ...input, operation: entry.operation || entry.provider.checkOperation, input: entry.input });
153
+ assert.deepEqual(result, state.response);
154
+ const request = requests.at(-1);
155
+ assert.equal(request.url.pathname, (entry.rotatedPathPrefix || "") + (entry.operationPath || entry.check));
156
+ if (entry.pathPrefix) assert.equal(request.headers.has("authorization"), false);
157
+ else if (entry.queryCredential) assert.equal(request.url.searchParams.get(entry.queryCredential), state.key);
158
+ else assert.equal(request.headers.get(entry.header), entry.rotatedHeader || entry.prefix + state.key);
159
+ for (const [key, value] of Object.entries(entry.query || {})) assert.equal(request.url.searchParams.get(key), value);
160
+ if (entry.body) assert.deepEqual(JSON.parse(request.init.body), entry.body);
161
+ await assert.rejects(restarted.invoke({ ...input, context: { ...input.context, applicationId: "app-two" }, operation: typeof entry.provider.checkOperation === "function" ? entry.provider.checkOperation("api-key") : entry.provider.checkOperation }), { code: "connector_reconnect_required" });
162
+ await restarted.disconnect(input);
163
+ assert.equal((await service.status(input)).status, "disconnected");
164
+ });
165
+
166
+ test(`${entry.provider.id}: rejects invalid inputs, malformed successes, invalid keys and rate limits without leaking credentials`, async (t) => {
167
+ const { service, input, state, requests } = await fixture(t, entry);
168
+ for (const [status, code] of [[200, "connector_response_invalid"], [401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"]]) {
169
+ state.status = status;
170
+ state.response = { error: { message: state.key } };
171
+ await assert.rejects(service.connectApiKey(input), (error) => {
172
+ assert.equal(error.code, code);
173
+ assert.equal(JSON.stringify(error).includes(state.key), false);
174
+ return true;
175
+ });
176
+ assert.equal((await service.status(input)).status, "disconnected");
177
+ }
178
+ state.status = 200;
179
+ state.response = entry.response;
180
+ await service.connectApiKey(input);
181
+ const count = requests.length;
182
+ await assert.rejects(service.invoke({ ...input, operation: entry.operation || entry.provider.checkOperation, input: { ...entry.input, unknown: "reject" } }), { code: "connector_input_invalid" });
183
+ assert.equal(requests.length, count);
184
+ state.status = 401;
185
+ await assert.rejects(service.invoke({ ...input, operation: typeof entry.provider.checkOperation === "function" ? entry.provider.checkOperation("api-key") : entry.provider.checkOperation }), { code: "connector_reconnect_required" });
186
+ assert.equal((await service.status(input)).status, "reconnect-required");
187
+ });
188
+ }
189
+
190
+ test("Clay advances only explicitly requested query pages and validates each result envelope", async (t) => {
191
+ const { service, input, state, requests } = await fixture(t, cases.find(({ provider }) => provider === clayProvider));
192
+ await service.connectApiKey(input);
193
+ assert.equal(requests.length, 1);
194
+ assert.equal(requests[0].url.pathname, "/public/v0/me");
195
+ state.response = { data: [{ clay_company_id: 123, name: "Fixture" }], has_more: true, source_type: "companies",
196
+ period_quota: { limit: 1000, used: 20, remaining: 980, resets_at: "2026-10-01T00:00:00Z" } };
197
+ assert.deepEqual(await service.invoke({ ...input, operation: "searches.next", input: { searchId: "search-1" } }), state.response);
198
+ assert.equal(requests.length, 2);
199
+ assert.equal(requests.at(-1).url.href, "https://api.clay.com/public/v0/search/query-mode/search-1/run");
200
+ assert.equal(requests.at(-1).init.method, "POST");
201
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { limit: 20 });
202
+ state.response = { data: [], has_more: false, source_type: "people", exhaustion_reason: "no_more_results" };
203
+ assert.deepEqual(await service.invoke({ ...input, operation: "searches.next", input: { searchId: "search_2", limit: 500 } }), state.response);
204
+ assert.equal(requests.length, 3);
205
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { limit: 500 });
206
+ const count = requests.length;
207
+ for (const invalid of [{}, { query: "" }, { query: " " }, { query: "x".repeat(16001) }, { query: "x", apiKey: "caller-key" }]) {
208
+ await assert.rejects(service.invoke({ ...input, operation: "searches.create", input: invalid }), { code: "connector_input_invalid" });
209
+ }
210
+ for (const invalid of [{}, { searchId: ".." }, { searchId: "a/b" }, { searchId: "https://other.invalid" },
211
+ { searchId: "search-1", limit: 0 }, { searchId: "search-1", limit: 501 }, { searchId: "search-1", limit: 1.5 }, { searchId: "search-1", page: 2 }]) {
212
+ await assert.rejects(service.invoke({ ...input, operation: "searches.next", input: invalid }), { code: "connector_input_invalid" });
213
+ }
214
+ assert.equal(requests.length, count);
215
+ for (const malformed of [{ data: [], source_type: "people" }, { data: {}, has_more: false, source_type: "companies" },
216
+ { data: [], has_more: false, source_type: "jobs" }]) {
217
+ state.response = malformed;
218
+ await assert.rejects(service.invoke({ ...input, operation: "searches.next", input: { searchId: "search-1" } }), { code: "connector_response_invalid" });
219
+ }
220
+ assert.equal(clayProvider.operations["identity.read"].validateResult({ user: { id: "user-1" }, workspace: {} }), false);
221
+ assert.equal(clayProvider.operations["searches.create"].validateResult({ search_id: "search-1", source_type: "jobs" }), false);
222
+ });
223
+
224
+ test("Clay search policy, billing failure and interrupted iterators never replay a request", async (t) => {
225
+ const { service, options, input, state, requests } = await fixture(t, cases.find(({ provider }) => provider === clayProvider));
226
+ await service.connectApiKey(input);
227
+ const restricted = createConnectionService({ ...options, authorize: async (owner, request) =>
228
+ request.operation === "searches.create" || request.operation === "searches.next" && request.input.searchId !== "search-1" ? null : owner });
229
+ await assert.rejects(restricted.invoke({ ...input, operation: "searches.create", input: { query: "anything" } }), { code: "connector_access_denied" });
230
+ await assert.rejects(restricted.invoke({ ...input, operation: "searches.next", input: { searchId: "another-search" } }), { code: "connector_access_denied" });
231
+ assert.equal(requests.length, 1);
232
+ state.status = 402;
233
+ state.response = { message: `Credit limit: ${state.key}` };
234
+ await assert.rejects(service.invoke({ ...input, operation: "searches.next", input: { searchId: "search-1" } }), (error) => {
235
+ assert.equal(error.code, "connector_provider_failed");
236
+ assert(!JSON.stringify(error).includes(state.key));
237
+ return true;
238
+ });
239
+ assert.equal(requests.length, 2);
240
+ let started;
241
+ const pendingRequest = new Promise((resolve) => { started = resolve; });
242
+ let attempts = 0;
243
+ const interrupted = createConnectionService({ ...options, fetchImpl: async (_url, init) => {
244
+ attempts += 1;
245
+ init.signal.throwIfAborted();
246
+ started();
247
+ return new Promise((resolve, reject) => init.signal.addEventListener("abort", () => reject(init.signal.reason), { once: true }));
248
+ } });
249
+ const controller = new AbortController();
250
+ const pending = assert.rejects(interrupted.invoke({ ...input, operation: "searches.next", input: { searchId: "search-1" }, signal: controller.signal }), { code: "connector_cancelled" });
251
+ await pendingRequest;
252
+ controller.abort();
253
+ await pending;
254
+ assert.equal(attempts, 1);
255
+ });
256
+
257
+ test("Clay runs approved routines and queries Enterprise tables without replay or hidden paging", async t => {
258
+ const { service, options, input, state, requests } = await fixture(t, cases.find(({ provider }) => provider === clayProvider));
259
+ await service.connectApiKey(input);
260
+ const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
261
+ state.response = { reference: "Provider-owned query grammar" };
262
+ assert.deepEqual(await invoke("searches.reference"), state.response);
263
+ assert.equal(requests.at(-1).url.pathname, "/public/v0/search/query-mode/reference");
264
+ state.response = { balance: 10, action_execution_balance: 20 };
265
+ assert.deepEqual(await invoke("credits.balance"), state.response);
266
+ const items = [{ id: "row-1", inputs: { domain: "example.test" } }];
267
+ state.response = { routine_run_id: "run-1", status: "in_progress" }; state.status = 202;
268
+ assert.deepEqual(await invoke("routines.run", { routineId: "function:t_example", items }), state.response);
269
+ assert.equal(requests.at(-1).url.pathname, "/public/v0/routines/function%3At_example/run");
270
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { items });
271
+ state.response = { routine_run_id: "run-1", total: 1, finished: 0, status: "in_progress" };
272
+ assert.deepEqual(await invoke("routines.results", { routineId: "run-1" }), state.response);
273
+ state.status = 200; state.response = { routine_run_id: "run-1", total: 1, finished: 1, status: "complete", data: [{ id: "row-1", error: "enrichment unavailable" }], cursor: "next+opaque" };
274
+ assert.deepEqual(await invoke("routines.results", { routineId: "run-1", cursor: "prior+&cursor", limit: 100 }), state.response);
275
+ assert.equal(requests.at(-1).url.searchParams.get("cursor"), "prior+&cursor");
276
+ const query = { tables: [{ id: "t_example" }], select: [{ field: "Domain", as: "domain" }], filter: { field: "Domain", op: "is_not_empty" }, field_mode: "names" };
277
+ state.response = { data: [{ domain: { value: "example.test" } }], fields: { domain: { name: "Domain" } }, truncated: true };
278
+ assert.deepEqual(await invoke("tables.query", { query, limit: 100 }), state.response);
279
+ assert.equal(requests.at(-1).url.pathname, "/public/v0/tables/query");
280
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query, limit: 100 });
281
+ const count = requests.length;
282
+ for (const [operation, values] of [["routines.run", { routineId: "../other", items }], ["routines.run", { routineId: "function:t_x", items: [] }],
283
+ ["routines.run", { routineId: "x", items: [{ id: "x" }] }], ["tables.query", { query: { tables: [] } }],
284
+ ["tables.query", { query, limit: 101 }], ["tables.query", { query: { ...query, url: "https://other.test" } }]])
285
+ await assert.rejects(invoke(operation, values), { code: "connector_input_invalid" });
286
+ const restricted = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "routines.run" ? null : owner });
287
+ await assert.rejects(restricted.invoke({ ...input, operation: "routines.run", input: { routineId: "x", items } }), { code: "connector_access_denied" });
288
+ assert.equal(requests.length, count);
289
+ state.response = { status: "complete" };
290
+ await assert.rejects(invoke("routines.results", { routineId: "run-1" }), { code: "connector_response_invalid" });
291
+ for (const status of [402, 403, 422, 429, 500]) { state.status = status; const before = requests.length; await assert.rejects(invoke("routines.run", { routineId: "function:t_x", items })); assert.equal(requests.length, before + 1); }
292
+ });
293
+
294
+ test("regional connections require re-verification before changing their provider destination", async (t) => {
295
+ for (const [provider, setting, alternative] of [
296
+ [paddleProvider, "environment", "live"], [mailgunProvider, "region", "eu"],
297
+ [gatewayApiProvider, "region", "eu"], [polarProvider, "environment", "production"],
298
+ [storyblokProvider, "region", "us"]
299
+ ]) {
300
+ const entry = cases.find((item) => item.provider === provider);
301
+ const { service, options, input, requests } = await fixture(t, entry);
302
+ await service.connectApiKey(input);
303
+ const config = structuredClone(options.configuration);
304
+ config.integrations.source.settings = { [setting]: alternative };
305
+ const changed = createConnectionService({ ...options, configuration: config });
306
+ assert.equal((await changed.status(input)).status, "reconnect-required");
307
+ await assert.rejects(changed.invoke({ ...input, operation: provider.checkOperation }), { code: "connector_reconnect_required" });
308
+ assert.equal(requests.length, 1);
309
+ await changed.connectApiKey(input);
310
+ assert.equal(requests.at(-1).url.origin, provider.apiOrigins[1]);
311
+ await changed.invoke({ ...input, operation: provider.checkOperation });
312
+ assert.equal(requests.at(-1).url.origin, provider.apiOrigins[1]);
313
+ assert.equal((await service.status(input)).status, "reconnect-required");
314
+ config.integrations.source.settings[setting] = "https://attacker.invalid";
315
+ const count = requests.length;
316
+ assert.throws(() => createConnectionService({ ...options, configuration: config }), { code: "integration_configuration_invalid" });
317
+ assert.equal(requests.length, count);
318
+ }
319
+ });
320
+
321
+ test("Storyblok supports all space regions and requires explicit draft reads without accepting caller credentials", async (t) => {
322
+ const entry = cases.find(({ provider }) => provider === storyblokProvider);
323
+ const { service, options, input, state, requests } = await fixture(t, entry);
324
+ for (const [region, host] of [
325
+ ["eu", "api.storyblok.com"], ["us", "api-us.storyblok.com"], ["ca", "api-ca.storyblok.com"],
326
+ ["ap", "api-ap.storyblok.com"], ["cn", "app.storyblokchina.cn"]
327
+ ]) {
328
+ const config = structuredClone(options.configuration);
329
+ config.integrations.source.settings = { region };
330
+ const regional = createConnectionService({ ...options, configuration: config });
331
+ state.response = entry.response;
332
+ await regional.connectApiKey(input);
333
+ assert.equal(requests.at(-1).url.host, host);
334
+ state.response = { stories: [] };
335
+ assert.deepEqual(await regional.invoke({ ...input, operation: "stories.list" }), state.response);
336
+ assert.deepEqual(Object.fromEntries(requests.at(-1).url.searchParams), {
337
+ version: "published", page: "1", per_page: "25", token: state.key
338
+ });
339
+ await regional.invoke({ ...input, operation: "stories.list", input: { version: "draft", per_page: 100 } });
340
+ assert.equal(requests.at(-1).url.searchParams.get("version"), "draft");
341
+ assert.equal(requests.at(-1).url.searchParams.get("per_page"), "100");
342
+ const count = requests.length;
343
+ for (const invalid of [{ token: "caller-key" }, { version: "all" }, { per_page: 0 }, { per_page: 101 }, { page: 0 }, { cv: -1 }]) {
344
+ await assert.rejects(regional.invoke({ ...input, operation: "stories.list", input: invalid }), { code: "connector_input_invalid" });
345
+ }
346
+ assert.equal(requests.length, count);
347
+ }
348
+ state.response = entry.response;
349
+ await service.connectApiKey(input);
350
+ state.status = 403;
351
+ state.response = { error: "Preview access required" };
352
+ await assert.rejects(service.invoke({ ...input, operation: "stories.list", input: { version: "draft" } }), { code: "connector_permission_denied" });
353
+ assert.equal(requests.at(-1).url.searchParams.get("version"), "draft");
354
+ assert.equal(storyblokProvider.operations["space.read"].validateResult({ space: { name: "incomplete" } }), false);
355
+ });
356
+
357
+ test("Storyblok delivers one page with resolved content and keeps private previews behind application policy", async (t) => {
358
+ const entry = cases.find(({ provider }) => provider === storyblokProvider);
359
+ const f = await fixture(t, entry);
360
+ await f.service.connectApiKey(f.input);
361
+ const content = { id: 123, full_slug: "bookings/groom & wash", content: { component: "page", title: "Grooming" } };
362
+ f.state.response = { story: content, rels: [{ id: 456, content: { component: "author", name: "Pat" } }],
363
+ assets: [{ id: 789, filename: "https://a.storyblok.com/f/123/photo.jpg", alt: "A groomed dog" }], links: [], cv: 42 };
364
+ assert.deepEqual(await f.service.invoke({ ...f.input, operation: "stories.get", input: {
365
+ id: content.full_slug, resolve_relations: "page.author", resolve_links: "url", resolve_assets: 1, cv: 42
366
+ } }), f.state.response);
367
+ const request = f.requests.at(-1);
368
+ assert.equal(request.init.method, "GET");
369
+ assert.equal(request.url.pathname, "/v2/cdn/stories/bookings/groom%20%26%20wash");
370
+ assert.deepEqual(Object.fromEntries(request.url.searchParams), { version: "published", cv: "42",
371
+ resolve_relations: "page.author", resolve_links: "url", resolve_assets: "1", token: f.state.key });
372
+ const restricted = createConnectionService({ ...f.options, authorize: async (owner, request) => request.input?.version === "draft" ? null : owner });
373
+ const before = f.requests.length;
374
+ await assert.rejects(restricted.invoke({ ...f.input, operation: "stories.get", input: { id: "home", version: "draft" } }), { code: "connector_access_denied" });
375
+ for (const id of ["../private", "a/../private", "a//b", "/home"]) {
376
+ await assert.rejects(f.service.invoke({ ...f.input, operation: "stories.get", input: { id } }), { code: "connector_input_invalid" });
377
+ }
378
+ assert.equal(f.requests.length, before);
379
+ f.state.response = { story: { id: 123, content: null } };
380
+ await assert.rejects(f.service.invoke({ ...f.input, operation: "stories.get", input: { id: "home" } }), { code: "connector_response_invalid" });
381
+ f.state.status = 404; f.state.response = { message: "Not found" };
382
+ await assert.rejects(f.service.invoke({ ...f.input, operation: "stories.get", input: { id: "unpublished" } }));
383
+ assert.equal(f.requests.at(-1).url.searchParams.get("version"), "published");
384
+ });
385
+
386
+ test("regional and account readers retain pagination defaults and reject oversized pages", () => {
387
+ for (const [provider, settings, size, maximum, defaults] of [
388
+ [paddleProvider, { environment: "sandbox" }, "per_page", 200, { per_page: "50", status: "active" }],
389
+ [mailgunProvider, { region: "us" }, "limit", 1000, { limit: "100", skip: "0", include_subaccounts: "false" }],
390
+ [fireworksAiProvider, {}, "pageSize", 200, { pageSize: "50" }],
391
+ [polarProvider, { environment: "sandbox" }, "limit", 100, { page: "1", limit: "10" }]
392
+ ]) {
393
+ const operation = provider.operations[provider.checkOperation];
394
+ const url = new URL(operation.request({}, settings).url);
395
+ assert.deepEqual(Object.fromEntries(url.searchParams), defaults);
396
+ for (const value of [0, maximum + 1]) assert.throws(() => operation.request({ [size]: value }, settings));
397
+ }
398
+ });
399
+
400
+ test("provider page sizes reject zero and values above the provider limit", () => {
401
+ for (const [provider, operation, field, maximum] of [
402
+ [notionProvider, "content.search", "page_size", 100], [brevoProvider, "contacts.list", "limit", 1000],
403
+ [elevenlabsProvider, "voices.list", "page_size", 100], [githubApiProvider, "repositories.list", "per_page", 100],
404
+ [apifyProvider, "actors.list", "limit", 1000], [hubspotProvider, "contacts.list", "limit", 100],
405
+ [linearProvider, "issues.list", "first", 100], [gitlabApiProvider, "projects.list", "per_page", 100],
406
+ [tallyProvider, "forms.list", "limit", 500], [contentfulProvider, "entries.list", "limit", 1000],
407
+ [asanaProvider, "workspaces.list", "limit", 100],
408
+ [incidentIoProvider, "incidents.list", "page_size", 500],
409
+ [firefliesProvider, "transcripts.list", "limit", 50], [heygenProvider, "voices.list", "limit", 100]
410
+ ]) {
411
+ for (const value of [0, maximum + 1]) assert.throws(() => provider.operations[operation].request({ [field]: value }), (error) => Boolean(error.fieldErrors[field]));
412
+ }
413
+ });
414
+
415
+ test("Linear rejects GraphQL errors even when the HTTP request and some data succeed", async (t) => {
416
+ const entry = cases.find(({ provider }) => provider === linearProvider);
417
+ const { service, input, state, requests } = await fixture(t, entry);
418
+ state.response = { ...entry.response, errors: [{ message: state.key, extensions: { code: "FORBIDDEN" } }] };
419
+ await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
420
+ assert.equal((await service.status(input)).status, "disconnected");
421
+ assert.deepEqual(JSON.parse(requests[0].init.body), { query: "query ConnectorViewer { viewer { id name email } }", variables: {} });
422
+ state.response = entry.response;
423
+ await service.connectApiKey(input);
424
+ state.response = { ...entry.operationResponse, errors: [{ message: state.key }] };
425
+ await assert.rejects(service.invoke({ ...input, operation: "issues.list" }), { code: "connector_response_invalid" });
426
+ });
427
+
428
+ test("Calendly requires a user URI and GitLab keeps project lists within membership", () => {
429
+ assert.throws(() => calendlyProvider.operations["eventTypes.list"].request({}), (error) => Boolean(error.fieldErrors.user));
430
+ for (const count of [0, 101]) {
431
+ assert.throws(() => calendlyProvider.operations["eventTypes.list"].request({ user: "https://api.calendly.com/users/fixture", count }), (error) => Boolean(error.fieldErrors.count));
432
+ }
433
+ for (const field of ["membership", "simple"]) {
434
+ assert.throws(() => gitlabApiProvider.operations["projects.list"].request({ [field]: false }), (error) => Boolean(error.fieldErrors[field]));
435
+ }
436
+ });
437
+
438
+
439
+ test("Fireflies rejects partial GraphQL errors and uses the authenticated user's profile", async (t) => {
440
+ const entry = cases.find(({ provider }) => provider === firefliesProvider);
441
+ const { service, input, state, requests } = await fixture(t, entry);
442
+ state.response = { ...entry.response, errors: [{ message: state.key }] };
443
+ await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
444
+ assert.equal((await service.status(input)).status, "disconnected");
445
+ assert.deepEqual(JSON.parse(requests[0].init.body), { query: "query ConnectorUser { user { user_id name email } }", variables: {} });
446
+ state.response = entry.response;
447
+ await service.connectApiKey(input);
448
+ state.response = { data: { transcript: { id: "meeting-1", title: "Planning", sentences: [{ speaker_name: "Ana", text: "Book the appointment", start_time: 1, end_time: 3 }], summary: { action_items: "Book appointment" } } } };
449
+ const detail = await service.invoke({ ...input, operation: "transcripts.get", input: { id: "meeting-1" } });
450
+ assert.equal(detail.data.transcript.sentences[0].text, "Book the appointment");
451
+ assert.equal(detail.data.transcript.summary.action_items, "Book appointment");
452
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { id: "meeting-1" });
453
+ state.response = { data: { transcript: null } };
454
+ await assert.rejects(service.invoke({ ...input, operation: "transcripts.get", input: { id: "missing" } }), { code: "connector_response_invalid" });
455
+ state.response = entry.operationResponse;
456
+ await service.invoke({ ...input, operation: "transcripts.search", input: { keyword: "appointment", skip: 25 } });
457
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { keyword: "appointment", scope: "all", limit: 25, skip: 25, mine: true });
458
+ await assert.rejects(service.invoke({ ...input, operation: "transcripts.search", input: { keyword: "", scope: "arbitrary" } }), { code: "connector_input_invalid" });
459
+ state.response = { ...entry.operationResponse, errors: [{ message: state.key }] };
460
+ await assert.rejects(service.invoke({ ...input, operation: "transcripts.list" }), { code: "connector_response_invalid" });
461
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { limit: 25, skip: 0, mine: true });
462
+ });
463
+
464
+ test("new read fragments preserve empty collections and reject invalid envelopes and boundaries", () => {
465
+ const incidents = incidentIoProvider.operations["incidents.list"];
466
+ assert.equal(incidents.validateResult({ incidents: [] }), true);
467
+ assert.equal(incidents.validateResult({ incidents: [], pagination_meta: null }), false);
468
+ assert.throws(() => incidents.request({ sort_by: "arbitrary" }));
469
+ const initial = new URL(incidents.request({}).url);
470
+ assert.equal(initial.searchParams.get("page_size"), "25");
471
+ assert.equal(initial.searchParams.get("sort_by"), "created_at_newest_first");
472
+ const balance = stripeProvider.operations["balance.read"];
473
+ assert.equal(balance.validateResult({ object: "balance", available: [], pending: [], livemode: true }), true);
474
+ assert.equal(balance.validateResult({ object: "balance", available: [], pending: [] }), false);
475
+ assert.equal(balance.request({}).url, "https://api.stripe.com/v1/balance");
476
+ const hardware = replicateProvider.operations["hardware.list"];
477
+ assert.equal(hardware.validateResult([]), true);
478
+ assert.equal(hardware.validateResult([null]), false);
479
+ for (const skip of [-1, 2147483648]) assert.throws(() => firefliesProvider.operations["transcripts.list"].request({ skip }));
480
+ });
481
+
482
+
483
+ test("AI service readers distinguish authenticated checks and preserve provider paging fields", () => {
484
+ assert.equal(perplexityProvider.checkOperation, "requests.list");
485
+ assert.equal(perplexityProvider.operations["requests.list"].validateResult({ requests: [], next_token: null }), true);
486
+ assert.equal(perplexityProvider.operations["requests.list"].validateResult({ object: "list", data: [] }), false);
487
+ assert.equal(supabaseProvider.operations["projects.list"].validateResult([]), true);
488
+ assert.equal(supabaseProvider.operations["projects.list"].validateResult([{ error: "no" }]), false);
489
+ const voices = heygenProvider.operations["voices.list"];
490
+ assert.equal(voices.validateResult({ data: [], has_more: false }), true);
491
+ assert.equal(voices.validateResult({ data: [], has_more: "false" }), false);
492
+ assert.throws(() => voices.request({ type: "arbitrary" }));
493
+ assert.throws(() => voices.request({ gender: "arbitrary" }));
494
+ const url = new URL(voices.request({}).url);
495
+ assert.equal(url.searchParams.get("limit"), "20");
496
+ assert.equal(url.searchParams.get("type"), "public");
497
+ });
498
+
499
+ test("Ashby preserves incremental-sync state and rejects HTTP-success errors or broken cursors", async (t) => {
500
+ const entry = cases.find((item) => item.provider === ashbyProvider);
501
+ const { service, input, state, requests } = await fixture(t, entry);
502
+ state.response = { success: false, errors: [state.key], errorInfo: { code: "next_cursor_expired", message: state.key } };
503
+ await assert.rejects(service.connectApiKey(input), (error) => {
504
+ assert.equal(error.code, "connector_provider_failed");
505
+ assert.equal(error.message.includes(state.key), false);
506
+ assert.equal(JSON.stringify(error).includes(state.key), false);
507
+ return true;
508
+ });
509
+ assert.equal((await service.status(input)).status, "disconnected");
510
+ state.response = entry.response;
511
+ await service.connectApiKey(input);
512
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { limit: 25 });
513
+ const count = requests.length;
514
+ for (const invalid of [{ limit: 0 }, { limit: 101 }, { limit: 1.5 }, { cursor: "" }, { cursor: "x".repeat(8193) }, { syncToken: "" }, { syncToken: "x".repeat(8193) }, { apiKey: "other" }, { url: "https://attacker.invalid" }]) {
515
+ await assert.rejects(service.invoke({ ...input, operation: "jobs.list", input: invalid }), { code: "connector_input_invalid" });
516
+ }
517
+ for (const key of ["key:password", "key with space", "key\nheader"]) {
518
+ state.key = key;
519
+ await assert.rejects(service.invoke({ ...input, operation: "jobs.list" }), { code: "connector_binding_missing" });
520
+ }
521
+ assert.equal(requests.length, count);
522
+ state.key = "private-fixture-key";
523
+ for (const response of [{ success: true, results: [] }, { ...entry.response, nextCursor: "" }, { ...entry.response, syncToken: {} }]) {
524
+ state.response = response;
525
+ await assert.rejects(service.invoke({ ...input, operation: "jobs.list" }), { code: "connector_response_invalid" });
526
+ }
527
+ state.response = { success: true, results: [], moreDataAvailable: false, syncToken: "next-sync" };
528
+ assert.deepEqual(await service.invoke({ ...input, operation: "jobs.list", input: { cursor: "next-cursor", syncToken: "current-sync" } }), state.response);
529
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { limit: 25, cursor: "next-cursor", syncToken: "current-sync" });
530
+ state.status = 403;
531
+ state.response = { success: false, errors: ["missing_endpoint_permission"] };
532
+ await assert.rejects(service.invoke({ ...input, operation: "jobs.list" }), { code: "connector_permission_denied" });
533
+ });
534
+
535
+ test("Lexware validates contact pages and role filters without accepting unsupported search or pagination values", async (t) => {
536
+ const entry = cases.find((item) => item.provider === lexwareProvider);
537
+ const { service, input, state, requests } = await fixture(t, entry);
538
+ await service.connectApiKey(input);
539
+ assert.deepEqual(Object.fromEntries(requests[0].url.searchParams), { page: "0", size: "25" });
540
+ for (const invalid of [{ page: -1 }, { page: 1.5 }, { size: 0 }, { size: 251 }, { size: 1.5 }, { customer: "maybe" }, { vendor: [] }, { name: "A&B" }]) {
541
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.list", input: invalid }), { code: "connector_input_invalid" });
542
+ }
543
+ assert.equal(requests.length, 1);
544
+ state.response = { content: [], first: true, last: true, totalPages: 0, totalElements: 0, numberOfElements: 0, size: 250, number: 0 };
545
+ assert.deepEqual(await service.invoke({ ...input, operation: "contacts.list", input: { size: 250, customer: true, vendor: false } }), state.response);
546
+ for (const response of [{ ...state.response, number: -1 }, { ...state.response, last: "true" }, { ...state.response, content: {} }, { ...state.response, size: 0 }]) {
547
+ state.response = response;
548
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.list" }), { code: "connector_response_invalid" });
549
+ }
550
+ });
551
+
552
+ test("Lexware reads accounting resources and creates explicit draft invoices without replay", async (t) => {
553
+ const entry = cases.find(item => item.provider === lexwareProvider);
554
+ const { service, options, input, state, requests } = await fixture(t, entry);
555
+ await service.connectApiKey(input);
556
+ const id = "11111111-1111-4111-8111-111111111111";
557
+ for (const resource of ["contacts", "articles", "invoices", "vouchers"]) {
558
+ state.response = { id, version: 1 };
559
+ assert.deepEqual(await service.invoke({ ...input, operation: `${resource}.get`, input: { id } }), state.response);
560
+ assert.equal(requests.at(-1).url.pathname, `/v1/${resource}/${id}`);
561
+ }
562
+ state.response = { ...entry.response, content: [{ id }], number: 1, last: false };
563
+ await service.invoke({ ...input, operation: "articles.list", input: { page: 1, type: "SERVICE" } });
564
+ assert.equal(requests.at(-1).url.pathname, "/v1/articles");
565
+ assert.equal(requests.at(-1).url.searchParams.get("type"), "SERVICE");
566
+ await service.invoke({ ...input, operation: "vouchers.list", input: { voucherType: "invoice", voucherStatus: "open", contactId: id } });
567
+ assert.equal(requests.at(-1).url.pathname, "/v1/voucherlist");
568
+ assert.equal(requests.at(-1).url.searchParams.get("contactId"), id);
569
+ const draft = { contactId: id, voucherDate: "2026-09-13T12:00:00+02:00", shippingDate: "2026-09-12T12:00:00+02:00", shippingType: "service",
570
+ lineItems: [{ name: "Grooming", quantity: 1, unitName: "session", netAmount: 50, taxRatePercentage: 19 }], title: "Booking invoice" };
571
+ state.response = { id, version: 1, resourceUri: "https://untrusted.invalid/not-followed" };
572
+ await service.invoke({ ...input, operation: "invoices.createDraft", input: draft });
573
+ const sent = requests.at(-1);
574
+ assert.equal(sent.init.method, "POST");
575
+ assert.equal(sent.url.href, "https://api.lexware.io/v1/invoices?finalize=false");
576
+ const body = JSON.parse(sent.init.body);
577
+ assert.deepEqual(body.address, { contactId: id });
578
+ assert.deepEqual(body.taxConditions, { taxType: "net" });
579
+ assert.deepEqual(body.lineItems[0].unitPrice, { currency: "EUR", netAmount: 50, taxRatePercentage: 19 });
580
+ assert.equal(body.lineItems[0].type, "custom");
581
+ const before = requests.length;
582
+ for (const invalid of [{ ...draft, lineItems: [] }, { ...draft, finalize: true }, { ...draft, contactId: "../other" },
583
+ { ...draft, voucherDate: "2026-02-30T00:00:00Z" }, { ...draft, lineItems: [{ ...draft.lineItems[0], netAmount: -1 }] },
584
+ { ...draft, lineItems: [{ ...draft.lineItems[0], taxRatePercentage: 101 }] }, { ...draft, lineItems: [{ ...draft.lineItems[0], currency: "USD" }] }])
585
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.createDraft", input: invalid }), { code: "connector_input_invalid" });
586
+ const restricted = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "invoices.createDraft" ? null : owner });
587
+ await assert.rejects(restricted.invoke({ ...input, operation: "invoices.createDraft", input: draft }), { code: "connector_access_denied" });
588
+ assert.equal(requests.length, before);
589
+ state.response = {};
590
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.createDraft", input: draft }), { code: "connector_response_invalid" });
591
+ for (const status of [403, 406, 429, 504]) {
592
+ state.status = status; const count = requests.length;
593
+ await assert.rejects(service.invoke({ ...input, operation: "invoices.createDraft", input: draft }));
594
+ assert.equal(requests.length, count + 1);
595
+ }
596
+ });
597
+
598
+ test("Sevdesk includes people by default, preserves numeric-string totals and bounds contact pagination", async (t) => {
599
+ const entry = cases.find((item) => item.provider === sevdeskProvider);
600
+ const { service, input, state, requests } = await fixture(t, entry);
601
+ await service.connectApiKey(input);
602
+ assert.deepEqual(Object.fromEntries(requests[0].url.searchParams), { limit: "100", offset: "0", countAll: "false", depth: "1" });
603
+ for (const invalid of [{ limit: 0 }, { limit: 1001 }, { limit: 1.5 }, { offset: -1 }, { offset: 1.5 }, { depth: 2 }, { countAll: "maybe" }, { Authorization: "other-token" }]) {
604
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.list", input: invalid }), { code: "connector_input_invalid" });
605
+ }
606
+ assert.equal(requests.length, 1);
607
+ for (const total of [undefined, "0", 0]) {
608
+ state.response = { objects: [], ...(total === undefined ? {} : { total }) };
609
+ assert.deepEqual(await service.invoke({ ...input, operation: "contacts.list", input: { limit: 1000, countAll: true, depth: 0 } }), state.response);
610
+ }
611
+ for (const response of [{ objects: {} }, { objects: [], total: -1 }, { objects: [], total: "-1" }, { objects: [], total: "unknown" }, { objects: [], total: 1.5 }]) {
612
+ state.response = response;
613
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.list" }), { code: "connector_response_invalid" });
614
+ }
615
+ });
616
+
617
+ test("Apollo searches saved accounts with bounded pages and cannot substitute a public health reply", async (t) => {
618
+ const entry = cases.find((entry) => entry.provider === apolloIoProvider);
619
+ const { service, input, state, requests } = await fixture(t, entry);
620
+ for (const response of [{ healthy: true, is_logged_in: false }, { organizations: [] }, { accounts: {} }]) {
621
+ state.response = response;
622
+ await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
623
+ assert.equal((await service.status(input)).status, "disconnected");
624
+ }
625
+ state.response = entry.response;
626
+ await service.connectApiKey(input);
627
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { page: 1, per_page: 25 });
628
+ assert.equal(requests.at(-1).headers.has("authorization"), false);
629
+ const count = requests.length;
630
+ for (const invalid of [{ page: 0 }, { page: 501 }, { page: 1.5 }, { per_page: 0 }, { per_page: 101 }, { per_page: 1.5 }, { q_organization_name: "" }, { q_organization_name: "x".repeat(257) }, { api_key: "other" }, { url: "https://attacker.invalid" }]) {
631
+ await assert.rejects(service.invoke({ ...input, operation: "accounts.search", input: invalid }), { code: "connector_input_invalid" });
632
+ }
633
+ assert.equal(requests.length, count);
634
+ state.response = { accounts: [], breadcrumbs: [] };
635
+ assert.deepEqual(await service.invoke({ ...input, operation: "accounts.search", input: { page: 500, per_page: 100 } }), state.response);
636
+ });
637
+
638
+ test("Attention uses the current conversation listing and preserves partial metadata without following returned URLs", async (t) => {
639
+ const entry = cases.find((entry) => entry.provider === attentionProvider);
640
+ const { service, input, state, requests } = await fixture(t, entry);
641
+ await service.connectApiKey(input);
642
+ assert.deepEqual(Object.fromEntries(requests[0].url.searchParams), { page: "1", size: "20", detailedTranscript: "false" });
643
+ for (const invalid of [{ page: 0 }, { page: 1.5 }, { size: 0 }, { size: 51 }, { size: 1.5 }, { detailedTranscript: "maybe" }, { "filter[title]": "" }, { "filter[title]": "x".repeat(501) }, { "filter[hide_internal]": [] }, { withCrmRecords: true }, { url: "https://attacker.invalid" }]) {
644
+ await assert.rejects(service.invoke({ ...input, operation: "conversations.list", input: invalid }), { code: "connector_input_invalid" });
645
+ }
646
+ assert.equal(requests.length, 1);
647
+ state.response = { data: [], meta: { totalRecords: 0 }, links: { related: "https://attacker.invalid" } };
648
+ assert.deepEqual(await service.invoke({ ...input, operation: "conversations.list", input: { page: 3, size: 50 } }), state.response);
649
+ assert.equal(requests.length, 2);
650
+ assert.equal(requests.at(-1).url.origin, "https://api.attention.tech");
651
+ state.response = { data: [] };
652
+ assert.deepEqual(await service.invoke({ ...input, operation: "conversations.list" }), state.response);
653
+ for (const response of [{ data: {} }, { data: [], meta: [] }, { data: [], meta: null }, { data: [], meta: { totalRecords: -1 } }, { data: [], meta: { pageCount: 1.5 } }]) {
654
+ state.response = response;
655
+ await assert.rejects(service.invoke({ ...input, operation: "conversations.list" }), { code: "connector_response_invalid" });
656
+ }
657
+ });
658
+
659
+
660
+ test("Telegram verifies bot identity without consuming updates and accepts polling webhook state", async (t) => {
661
+ const entry = cases.find((entry) => entry.provider === telegramProvider);
662
+ const { service, input, state, requests } = await fixture(t, entry);
663
+ await service.connectApiKey(input);
664
+ state.response = { ok: true, result: { url: "", has_custom_certificate: false, pending_update_count: 0 } };
665
+ assert.deepEqual(await service.invoke({ ...input, operation: "webhook.read" }), state.response);
666
+ for (const response of [
667
+ { ok: true, result: { url: "", has_custom_certificate: false, pending_update_count: -1 } },
668
+ { ok: true, result: { url: "", pending_update_count: 0 } }
669
+ ]) {
670
+ state.response = response;
671
+ await assert.rejects(service.invoke({ ...input, operation: "webhook.read" }), { code: "connector_response_invalid" });
672
+ }
673
+ const count = requests.length;
674
+ await assert.rejects(service.invoke({ ...input, operation: "getUpdates" }), { code: "connector_operation_unknown" });
675
+ for (const invalid of [{ offset: 2 }, { token: "other" }, { url: "https://attacker.invalid" }]) {
676
+ await assert.rejects(service.invoke({ ...input, operation: "webhook.read", input: invalid }), { code: "connector_input_invalid" });
677
+ }
678
+ for (const key of ["123:has/slash", "123:has?query", "../token", "123:has space", "123:"]) {
679
+ state.key = key;
680
+ await assert.rejects(service.connectApiKey(input), { code: "connector_binding_invalid" });
681
+ }
682
+ assert.equal(requests.length, count);
683
+ assert.ok(requests.every(({ url }) => ["/bot123456:fixture_token/getMe", "/bot123456:fixture_token/getWebhookInfo"].includes(url.pathname)));
684
+ });
685
+
686
+ test("Telegram rejects user identities and HTTP-200 failure envelopes without echoing descriptions", async (t) => {
687
+ const entry = cases.find((entry) => entry.provider === telegramProvider);
688
+ const { service, input, state } = await fixture(t, entry);
689
+ for (const bot of [{ id: 12, is_bot: false, first_name: "User" }, { id: 1.5, is_bot: true, first_name: "Bot" }, { id: 12, is_bot: true }]) {
690
+ state.response = { ok: true, result: bot };
691
+ await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
692
+ assert.equal((await service.status(input)).status, "disconnected");
693
+ }
694
+ state.response = entry.response;
695
+ await service.connectApiKey(input);
696
+ for (const [status, code] of [[403, "connector_permission_denied"], [429, "connector_rate_limited"], [400, "connector_provider_failed"], [401, "connector_reconnect_required"]]) {
697
+ state.response = { ok: false, error_code: status, description: state.key, parameters: { retry_after: 10 } };
698
+ await assert.rejects(service.invoke({ ...input, operation: "profile.read" }), (error) => {
699
+ assert.equal(error.code, code);
700
+ assert.equal(error.stack.includes(state.key), false);
701
+ assert.equal(error.cause, undefined);
702
+ return true;
703
+ });
704
+ }
705
+ assert.equal((await service.status(input)).status, "reconnect-required");
706
+ });
707
+
708
+ test("KLIPY validates clip search bounds, encodes its path key and rejects unsuccessful envelopes", async (t) => {
709
+ const entry = cases.find((entry) => entry.provider === klipyProvider);
710
+ const { service, input, state, requests } = await fixture(t, entry);
711
+ state.key = "key /?#%";
712
+ await service.connectApiKey(input);
713
+ assert.equal(requests[0].url.pathname, "/api/v1/key%20%2F%3F%23%25/clips/trending");
714
+ assert.equal(requests[0].url.search, "?page=1&per_page=24&content_filter=high");
715
+ for (const invalid of [{}, { q: "" }, { q: "x".repeat(501) }, { q: "test", page: 0 }, { q: "test", page: 1.5 },
716
+ { q: "test", per_page: 7 }, { q: "test", per_page: 51 }, { q: "test", per_page: 8.5 },
717
+ { q: "test", locale: "en_US" }, { q: "test", customer_id: "" }, { q: "test", content_filter: "strict" },
718
+ { q: "test", app_key: "another" }, { q: "test", url: "https://attacker.invalid" }]) {
719
+ await assert.rejects(service.invoke({ ...input, operation: "clips.search", input: invalid }), { code: "connector_input_invalid" });
720
+ }
721
+ assert.equal(requests.length, 1);
722
+ for (const key of [".", ".."]) {
723
+ state.key = key;
724
+ await assert.rejects(service.connectApiKey(input), { code: "connector_request_invalid" });
725
+ }
726
+ assert.equal(requests.length, 1);
727
+ state.key = "replacement-key";
728
+ state.response = { result: true, data: { data: [], next: false } };
729
+ assert.deepEqual(await service.invoke({ ...input, operation: "clips.search", input: { q: "nothing" } }), state.response);
730
+ for (const [response, code] of [
731
+ [{ result: false, data: { data: [], message: state.key } }, "connector_provider_failed"],
732
+ [{ result: true, data: [] }, "connector_response_invalid"],
733
+ [{ result: true, data: { data: {} } }, "connector_response_invalid"]
734
+ ]) {
735
+ state.response = response;
736
+ await assert.rejects(service.invoke({ ...input, operation: "clips.trending" }), (error) => {
737
+ assert.equal(error.code, code);
738
+ assert.equal(error.stack.includes(state.key), false);
739
+ return true;
740
+ });
741
+ }
742
+ });
743
+
744
+
745
+ test("KLIPY media families preserve embedding metadata and never fetch media", async (t) => {
746
+ const entry = cases.find((entry) => entry.provider === klipyProvider);
747
+ const { service, input, state, requests } = await fixture(t, entry);
748
+ await service.connectApiKey(input);
749
+ for (const family of ["gifs", "stickers", "emojis"]) {
750
+ state.response = { result: true, data: { data: [
751
+ { type: family, slug: "happy-dog", file: { hd: { webp: { url: "https://static.klipy.com/dog.webp?delivery=keep" } } } },
752
+ { type: "ad", id: "ad-1", tracking: "preserve" }
753
+ ], has_next: true, current_page: 2 } };
754
+ for (const action of ["trending", "search"]) {
755
+ const result = await service.invoke({ ...input, operation: `${family}.${action}`,
756
+ input: { page: 2, per_page: 8, ...(action === "search" ? { q: "happy dog" } : {}) } });
757
+ assert.deepEqual(result, state.response);
758
+ const request = requests.at(-1);
759
+ assert.equal(request.url.pathname, `/api/v1/${state.key}/${family}/${action}`);
760
+ assert.equal(request.url.searchParams.get("page"), "2");
761
+ assert.equal(request.url.searchParams.get("content_filter"), "high");
762
+ if (action === "search") assert.equal(request.url.searchParams.get("q"), "happy dog");
763
+ }
764
+ }
765
+ assert.equal(requests.length, 7);
766
+ assert(requests.every(({ url }) => url.origin === "https://api.klipy.com"));
767
+ await assert.rejects(service.invoke({ ...input, operation: "gifs.search", input: { q: "dog", per_page: 7 } }), { code: "connector_input_invalid" });
768
+ assert.equal(requests.length, 7);
769
+ });
770
+
771
+
772
+ test("Contentful uses the selected delivery space and rejects invalid settings before transport", async (t) => {
773
+ const entry = cases.find(({ provider }) => provider === contentfulProvider);
774
+ const { options, input, requests } = await fixture(t, entry);
775
+ const configured = (settings) => ({ ...options, configuration: {
776
+ ...options.configuration, integrations: { source: {
777
+ ...options.configuration.integrations.source, settings
778
+ } }
779
+ } });
780
+ for (const settings of [{}, { spaceId: "../other" }, { spaceId: "space-one", environmentId: "../../other" },
781
+ { spaceId: "space-one", region: "elsewhere" }]) {
782
+ assert.throws(() => createConnectionService(configured(settings)));
783
+ }
784
+ assert.equal(requests.length, 0);
785
+ const service = createConnectionService(configured({ spaceId: "space-one", environmentId: "preview-2", region: "eu" }));
786
+ await service.connectApiKey(input);
787
+ assert.equal(requests[0].url.href, "https://cdn.eu.contentful.com/spaces/space-one/environments/preview-2/entries?limit=1&skip=0");
788
+ assert.equal(requests[0].headers.get("authorization"), "Bearer private-fixture-key");
789
+ assert.equal(requests[0].url.searchParams.has("access_token"), false);
790
+ const operation = contentfulProvider.operations["entries.list"];
791
+ assert.equal(operation.validateResult({ sys: { type: "Array" }, items: [{ sys: { type: "Entry", id: "entry-1" } }], total: 1, limit: 1, skip: 0 }), true);
792
+ for (const items of [[null], [{ sys: { type: "Space", id: "space-one" } }]]) {
793
+ assert.equal(operation.validateResult({ ...entry.response, items }), false);
794
+ }
795
+ });
796
+
797
+
798
+ test("Contentful delivers published localized entries and linked assets without fetching returned URLs", async t => {
799
+ const { service, input, state, requests } = await fixture(t, cases.find(({ provider }) => provider === contentfulProvider));
800
+ await service.connectApiKey(input);
801
+ const asset = { sys: { type: "Asset", id: "hero" }, fields: { file: { url: "//images.ctfassets.net/space/hero/photo.jpg" } } };
802
+ const article = { sys: { type: "Entry", id: "article" }, fields: { title: { "en-US": "Published story" }, image: { sys: { type: "Link", linkType: "Asset", id: "hero" } } } };
803
+ state.response = { sys: { type: "Array" }, items: [article], includes: { Asset: [asset] }, errors: [{ sys: { id: "notResolvable", type: "error" }, details: { id: "unpublished-link" } }], total: 3, skip: 0, limit: 1 };
804
+ const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
805
+ assert.deepEqual(await invoke("entries.list", { locale: "*", include: 10, content_type: "article", order: "sys.createdAt", "sys.id": "article" }), state.response);
806
+ assert.equal(requests.at(-1).url.searchParams.get("locale"), "*"); assert.equal(requests.at(-1).url.searchParams.get("include"), "10");
807
+ state.response = { ...state.response, items: [], includes: {}, skip: 2 };
808
+ assert.deepEqual(await invoke("entries.list", { skip: 2 }), state.response);
809
+ for (const [operation, path, item] of [["entries.get", "entries/article", article], ["assets.get", "assets/hero", asset], ["contentTypes.get", "content_types/model", { sys: { type: "ContentType", id: "model" }, fields: [{ id: "title", type: "Text" }] }]]) {
810
+ state.response = item; assert.deepEqual(await invoke(operation, { id: item.sys.id }), item);
811
+ assert.equal(requests.at(-1).url.pathname, `/spaces/space-one/environments/master/${path}`);
812
+ }
813
+ for (const [operation, type] of [["assets.list", "Asset"], ["contentTypes.list", "ContentType"], ["locales.list", "Locale"]]) {
814
+ state.response = { sys: { type: "Array" }, items: [{ sys: { type, id: "resource" } }], skip: 1, limit: 2, total: 3 };
815
+ assert.deepEqual(await invoke(operation, { skip: 1, limit: 2 }), state.response);
816
+ }
817
+ assert(requests.every(request => request.init.method === "GET" && request.url.origin === "https://cdn.contentful.com"));
818
+ const before = requests.length;
819
+ for (const [operation, values] of [["entries.get", { id: "../outside" }], ["entries.list", { include: 11 }], ["assets.get", { id: "hero", url: "https://other.test" }]])
820
+ await assert.rejects(invoke(operation, values), { code: "connector_input_invalid" });
821
+ assert.equal(requests.length, before);
822
+ state.response = { sys: { type: "Entry", id: "wrong-type" } };
823
+ await assert.rejects(invoke("assets.get", { id: "hero" }), { code: "connector_response_invalid" });
824
+ state.status = 404; await assert.rejects(invoke("entries.get", { id: "unpublished" }));
825
+ });
826
+
827
+
828
+ test("GatewayAPI sends regional SMS/RCS batches and verifies current Messaging callback signatures", async t => {
829
+ const entry = cases.find(({ provider }) => provider === gatewayApiProvider);
830
+ for (const region of ["global", "eu"]) {
831
+ const { service, input, state, requests } = await fixture(t, { ...entry, settings: { region } });
832
+ await service.connectApiKey(input);
833
+ const message = { sender: "Bookings", recipient: 4512345678, message: "Ready", reference: "booking-one" };
834
+ const receipt = { msg_id: "message-one", recipient: message.recipient, reference: message.reference };
835
+ state.response = receipt;
836
+ assert.deepEqual(await service.invoke({ ...input, operation: "messages.send", input: message }), receipt);
837
+ assert.equal(requests.at(-1).url.href, `https://messaging.gatewayapi.${region === "eu" ? "eu" : "com"}/mobile/single`);
838
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { ...message, priority: "normal" });
839
+ state.response = { responses: [receipt] };
840
+ await service.invoke({ ...input, operation: "messages.sendBatch", input: { messages: [message] } });
841
+ assert.equal(requests.at(-1).url.pathname, "/mobile/multi");
842
+ const before = requests.length;
843
+ for (const invalid of [{ ...message, recipient: -1 }, { ...message, reference: "" }, { ...message, sender: "TooLongSenderName" }]) await assert.rejects(service.invoke({ ...input, operation: "messages.send", input: invalid }), { code: "connector_input_invalid" });
844
+ await assert.rejects(service.invoke({ ...input, operation: "messages.sendBatch", input: { messages: [] } }), { code: "connector_input_invalid" });
845
+ assert.equal(requests.length, before);
846
+ state.response = { responses: [{}] };
847
+ await assert.rejects(service.invoke({ ...input, operation: "messages.sendBatch", input: { messages: [message] } }), { code: "connector_response_invalid" });
848
+ state.status = 500; const failedBefore = requests.length;
849
+ await assert.rejects(service.invoke({ ...input, operation: "messages.send", input: message }), { code: "connector_provider_failed" });
850
+ assert.equal(requests.length, failedBefore + 1);
851
+ }
852
+ const secret = "fixture-webhook-secret";
853
+ for (const event_type of ["message.status.sms", "message.status.rcs", "user-message.text.sms", "user-message.text.rcs"]) {
854
+ const event = { event_id: "event-one", timestamp: "2026-09-13T00:00:00Z", event_type, event: { msg_id: "message-one", reference: "booking-one", status: "DELIVERED" } };
855
+ const rawBody = Buffer.from(JSON.stringify(event));
856
+ const signature = "v1=" + createHmac("sha256", secret).update(rawBody).digest("hex");
857
+ assert.deepEqual(verifyGatewayApiEvent({ rawBody, signature, secret }), event);
858
+ for (const override of [{ secret: "wrong" }, { rawBody: Buffer.from("{}") }, { signature: "legacy.jwt.value" }]) assert.throws(() => verifyGatewayApiEvent({ rawBody, signature, secret, ...override }), { code: "connector_webhook_invalid" });
859
+ }
860
+ });
861
+
862
+ test("HubSpot leads become linked deals and advance through a pipeline with app-owned policy", async t => {
863
+ const entry = cases.find(item => item.provider.id === "hubspot");
864
+ const f = await fixture(t, { ...entry, scopes: hubspotProvider.scopes.map(scope => scope.value) });
865
+ const calls = [];
866
+ const service = createConnectionService({ ...f.options,
867
+ authorize: async (owner, request) => request.operation === "deals.update" && request.input.id !== "202" ? null : owner,
868
+ fetchImpl: async (address, init) => {
869
+ const url = new URL(address), body = init.body ? JSON.parse(init.body) : undefined;
870
+ calls.push({ url, method: init.method, body });
871
+ assert.equal(new Headers(init.headers).get("authorization"), "Bearer private-fixture-key");
872
+ if (url.pathname.includes("/pipelines/")) return Response.json({ results: [{ id: "sales", stages: [{ id: "qualified" }, { id: "won" }] }] });
873
+ if (url.pathname.includes("/associations/default/")) return Response.json({ status: "COMPLETE", results: [{ from: { id: "202" }, to: { id: "101" } }] });
874
+ if (init.method === "POST" || init.method === "PATCH") return Response.json({ id: url.pathname.includes("deals") ? "202" : "101", properties: body.properties });
875
+ if (/\/[0-9]+$/u.test(url.pathname)) return Response.json({ id: url.pathname.split("/").at(-1), properties: { email: "customer@example.test" }, associations: { contacts: { results: [{ id: "101" }] } } });
876
+ return Response.json({ results: [], paging: { next: { after: "next-page" } } });
877
+ }
878
+ });
879
+ await service.connectApiKey(f.input);
880
+ const invoke = (operation, input = {}) => service.invoke({ ...f.input, operation, input });
881
+ const pipeline = (await invoke("pipelines.list")).results[0];
882
+ const contact = await invoke("contacts.create", { properties: { email: "customer@example.test", firstname: "Customer" } });
883
+ const deal = await invoke("deals.create", { properties: { dealname: "Grooming booking", pipeline: pipeline.id, dealstage: pipeline.stages[0].id, amount: "80.00" } });
884
+ await invoke("deals.associateContact", { contactId: contact.id, dealId: deal.id });
885
+ assert.equal(calls.at(-1).method, "PUT");
886
+ assert.equal(calls.at(-1).url.pathname, "/crm/objects/2026-09/deals/202/associations/default/contacts/101");
887
+ const advanced = await invoke("deals.update", { id: deal.id, properties: { dealstage: "won" } });
888
+ assert.equal(advanced.properties.dealstage, "won");
889
+ assert.equal(calls.at(-1).method, "PATCH");
890
+ assert.deepEqual(calls.at(-1).body, { properties: { dealstage: "won" } });
891
+ await invoke("contacts.update", { id: contact.id, properties: { phone: "" } });
892
+ assert.deepEqual(calls.at(-1).body, { properties: { phone: "" } });
893
+ const read = await invoke("deals.get", { id: deal.id, properties: "dealname,dealstage", associations: "contacts" });
894
+ assert.equal(read.associations.contacts.results[0].id, contact.id);
895
+ assert.equal(calls.at(-1).url.searchParams.get("properties"), "dealname,dealstage");
896
+ await invoke("contacts.get", { id: contact.id });
897
+ assert.equal((await invoke("deals.list", { after: "next & page", limit: 2 })).paging.next.after, "next-page");
898
+ assert.equal(calls.at(-1).url.searchParams.get("after"), "next & page");
899
+ const before = calls.length;
900
+ for (const [operation, input] of [
901
+ ["deals.update", { id: "999", properties: { amount: "1" } }],
902
+ ["deals.create", { properties: { dealname: "Missing stage" } }],
903
+ ["contacts.create", { properties: { phone: "123" } }],
904
+ ["contacts.update", { id: "101", properties: { amount: 1 } }],
905
+ ["contacts.get", { id: "../contacts" }],
906
+ ["deals.associateContact", { contactId: "101/other", dealId: "202" }]
907
+ ]) await assert.rejects(invoke(operation, input));
908
+ assert.equal(calls.length, before);
909
+ const limited = createConnectionService({ ...f.options, configuration: { ...f.options.configuration,
910
+ integrations: { source: { ...f.options.configuration.integrations.source, scopes: ["crm.objects.contacts.read"] } } } });
911
+ await assert.rejects(limited.invoke({ ...f.input, operation: "deals.create", input: { properties: { dealname: "Denied", dealstage: "won" } } }), { code: "connector_scope_missing" });
912
+ assert.equal(hubspotProvider.operations["contacts.create"].validateResult({ status: "error" }), false);
913
+ assert.equal(hubspotProvider.operations["deals.associateContact"].validateResult({}), false);
914
+ });
915
+
916
+ test("incident.io incident recovery covers follow-ups, alerts, schedules and catalogue with explicit writes", async t => {
917
+ const f = await fixture(t, cases.find(item => item.provider.id === "incident-io"));
918
+ const calls = [];
919
+ let rejected = false;
920
+ const service = createConnectionService({ ...f.options,
921
+ authorize: async (owner, request) => request.operation === "alerts.resolve" && request.input.id !== "alert-one" ? null : owner,
922
+ fetchImpl: async (address, init) => {
923
+ const url = new URL(address), body = init.body ? JSON.parse(init.body) : undefined;
924
+ calls.push({ url, method: init.method, body });
925
+ assert.equal(new Headers(init.headers).get("authorization"), "Bearer private-fixture-key");
926
+ if (rejected) return Response.json({ message: "Private fixture detail" }, { status: 422 });
927
+ const name = url.pathname.split("/")[2];
928
+ const singular = { incidents: "incident", follow_ups: "follow_up", alerts: "alert", schedules: "schedule", catalog_entries: "catalog_entry" }[name];
929
+ if (init.method !== "GET" || url.pathname.split("/").length > 3) return Response.json({ [singular]: { id: "resource-one", ...body } });
930
+ return Response.json({ [name]: [{ id: "resource-one", ...(name === "schedules" ? { next_shifts: [{ user_id: "responder" }] } : {}) }], pagination_meta: { page_size: 25, after: "next-page" } });
931
+ }
932
+ });
933
+ await service.connectApiKey(f.input);
934
+ const invoke = (operation, input = {}) => service.invoke({ ...f.input, operation, input });
935
+ const incident = await invoke("incidents.create", { name: "API unavailable", idempotency_key: "outage-event-one", visibility: "private", mode: "test" });
936
+ assert.equal(incident.incident.id, "resource-one");
937
+ assert.equal(calls.at(-1).body.idempotency_key, "outage-event-one");
938
+ await invoke("incidents.update", { id: "resource-one", incident: { summary: "Recovered" }, notify_incident_channel: false });
939
+ assert.deepEqual(calls.at(-1).body, { incident: { summary: "Recovered" }, notify_incident_channel: false });
940
+ await invoke("followUps.create", { incident_id: "resource-one", title: "Add retry coverage", assignee_id: "responder" });
941
+ await invoke("followUps.update", { id: "follow-one", title: "Add retry coverage", status: "completed" });
942
+ assert.equal(calls.at(-1).method, "PUT");
943
+ await invoke("followUps.list", { incident_id: "resource-one", incident_mode: "test" });
944
+ assert.equal(calls.at(-1).url.searchParams.get("incident_mode"), "test");
945
+ await invoke("alerts.resolve", { id: "alert-one" });
946
+ assert.equal(calls.at(-1).url.pathname, "/v2/alerts/alert-one/actions/resolve");
947
+ assert.equal(calls.at(-1).method, "POST");
948
+ await invoke("alerts.list", { page_size: 50, after: "next & page" });
949
+ assert.equal(calls.at(-1).url.searchParams.get("after"), "next & page");
950
+ const schedules = await invoke("schedules.list");
951
+ assert.equal(schedules.schedules[0].next_shifts[0].user_id, "responder");
952
+ assert.equal(calls.at(-1).url.searchParams.get("page_size"), "25");
953
+ await invoke("catalog.types");
954
+ await invoke("catalog.list", { catalog_type_id: "services", identifier: "payments", after: "next" });
955
+ const entry = { name: "Payments", external_id: "payments", attribute_values: { owner: { value: { literal: "team-one" } } } };
956
+ await invoke("catalog.create", { catalog_type_id: "services", ...entry });
957
+ assert.deepEqual(calls.at(-1).body.attribute_values, entry.attribute_values);
958
+ await invoke("catalog.update", { id: "catalog-one", ...entry });
959
+ assert.equal(calls.at(-1).method, "PUT");
960
+ for (const operation of ["incidents.get", "followUps.get", "alerts.get", "schedules.get", "catalog.get"]) await invoke(operation, { id: "resource-one" });
961
+ const before = calls.length;
962
+ for (const [operation, input] of [
963
+ ["incidents.create", { name: "No explicit visibility", idempotency_key: "one" }],
964
+ ["incidents.update", { id: "one", incident: { summary: "No notification choice" } }],
965
+ ["followUps.update", { id: "one", title: "task", status: "arbitrary" }],
966
+ ["alerts.resolve", { id: "other-user" }], ["catalog.get", { id: "../keys" }],
967
+ ["alerts.list", { page_size: 51 }], ["schedules.list", { page_size: 26 }],
968
+ ["catalog.create", { catalog_type_id: "services", name: "Payments", attribute_values: { owner: { value: { literal: 42 } } } }]
969
+ ]) await assert.rejects(invoke(operation, input));
970
+ assert.equal(calls.length, before);
971
+ rejected = true;
972
+ await assert.rejects(invoke("alerts.resolve", { id: "alert-one" }), error => !JSON.stringify(error).includes("Private fixture detail"));
973
+ assert.equal(calls.length, before + 1);
974
+ for (const operation of ["incidents.create", "followUps.create", "alerts.resolve", "catalog.update"]) assert.equal(incidentIoProvider.operations[operation].validateResult({}), false);
975
+ });
976
+
977
+ test("Notion reads content and data sources then creates and updates a note with bounded writes", async t => {
978
+ const { service, options, input, state, requests } = await fixture(t, cases.find(entry => entry.provider === notionProvider));
979
+ await service.connectApiKey(input);
980
+ const id = "d9824bdc-8445-4327-be8b-5b47500af6ce";
981
+ const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
982
+ for (const [operation, object, collection] of [["pages.get", "page", "pages"], ["databases.get", "database", "databases"], ["dataSources.get", "data_source", "data_sources"]]) {
983
+ state.response = { object, id, properties: { Name: { type: "title" } } };
984
+ assert.deepEqual(await invoke(operation, { id }), state.response);
985
+ assert.equal(requests.at(-1).url.pathname, `/v1/${collection}/${id}`);
986
+ }
987
+ state.response = { object: "list", results: [{ object: "block", id, has_children: true }], has_more: true, next_cursor: "next+cursor" };
988
+ assert.deepEqual(await invoke("blocks.list", { id, start_cursor: "prior+&cursor", page_size: 2 }), state.response);
989
+ assert.equal(requests.at(-1).url.searchParams.get("start_cursor"), "prior+&cursor");
990
+ const filter = { property: "Done", checkbox: { equals: false } };
991
+ await invoke("dataSources.query", { id, filter });
992
+ assert.equal(requests.at(-1).url.pathname, `/v1/data_sources/${id}/query`);
993
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { page_size: 100, filter });
994
+ const properties = { Name: { title: [{ text: { content: "Meeting" } }] } };
995
+ const children = [{ object: "block", type: "paragraph", paragraph: { rich_text: [{ text: { content: "Action" } }] } }];
996
+ state.response = { object: "page", id, properties };
997
+ await invoke("pages.create", { parentId: id, parentType: "data_source_id", properties, children });
998
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { parent: { type: "data_source_id", data_source_id: id }, properties, children });
999
+ await invoke("pages.update", { id, properties });
1000
+ assert.equal(requests.at(-1).init.method, "PATCH");
1001
+ state.response = { object: "list", results: children, has_more: false };
1002
+ await invoke("blocks.append", { id, children });
1003
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { children });
1004
+ const before = requests.length;
1005
+ for (const [op, values] of [["pages.get", { id: "../users" }], ["blocks.append", { id, children: [] }],
1006
+ ["blocks.append", { id, children: Array(101).fill(children[0]) }], ["pages.update", { id, properties: { x: "x".repeat(65536) } }]])
1007
+ await assert.rejects(invoke(op, values), { code: "connector_input_invalid" });
1008
+ const restricted = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "pages.create" ? null : owner });
1009
+ await assert.rejects(restricted.invoke({ ...input, operation: "pages.create", input: { parentId: id, parentType: "page_id", properties } }), { code: "connector_access_denied" });
1010
+ assert.equal(requests.length, before);
1011
+ for (const status of [403, 404, 429, 500]) { state.status = status; const count = requests.length; await assert.rejects(invoke("blocks.append", { id, children })); assert.equal(requests.length, count + 1); }
1012
+ });
1013
+
1014
+ test("Pipedrive token CRM reads and writes use its configured company and preserve paging", async t => {
1015
+ const entry = cases.find(entry => entry.provider === pipedriveProvider);
1016
+ const { service, options, input, state, requests } = await fixture(t, { ...entry, settings: { companyDomain: "acme" } });
1017
+ await service.connectApiKey(input);
1018
+ const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
1019
+ for (const collection of ["deals", "persons", "organizations", "activities", "leads", "pipelines"]) {
1020
+ const lead = collection === "leads";
1021
+ const id = lead ? "d9824bdc-8445-4327-be8b-5b47500af6ce" : 12;
1022
+ state.response = { success: true, data: [{ id }], additional_data: { next_cursor: "next+&2" } };
1023
+ assert.deepEqual(await invoke(`${collection}.list`, lead ? { start: 20, limit: 10 } : { cursor: "prior+&1", limit: 10 }), state.response);
1024
+ assert.equal(requests.at(-1).url.origin, "https://acme.pipedrive.com");
1025
+ assert.equal(requests.at(-1).url.pathname, `/api/${lead ? "v1" : "v2"}/${collection}`);
1026
+ assert.equal(requests.at(-1).headers.get("x-api-token"), state.key);
1027
+ state.response = { success: true, data: { id } };
1028
+ await invoke(`${collection}.get`, { id });
1029
+ if (collection === "pipelines") continue;
1030
+ const values = ["deals", "leads"].includes(collection) ? { title: "Follow-up", person_id: 1 }
1031
+ : collection === "activities" ? { subject: "Call", type: "call", done: false } : { name: "Customer" };
1032
+ await invoke(`${collection}.create`, values);
1033
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), values);
1034
+ await invoke(`${collection}.update`, { id, ...values }); assert.equal(requests.at(-1).init.method, "PATCH");
1035
+ }
1036
+ const count = requests.length;
1037
+ for (const [op, values] of [["deals.get", { id: "../other" }], ["leads.create", { title: "No contact" }], ["deals.update", { id: 1 }], ["persons.create", { name: "X", emails: Array(21).fill({ value: "x@y.test" }) }]])
1038
+ await assert.rejects(invoke(op, values), { code: "connector_input_invalid" });
1039
+ const denied = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "deals.create" ? null : owner });
1040
+ await assert.rejects(denied.invoke({ ...input, operation: "deals.create", input: { title: "Denied" } }), { code: "connector_access_denied" });
1041
+ assert.equal(requests.length, count);
1042
+ for (const status of [403, 429, 500]) { state.status = status; const before = requests.length; await assert.rejects(invoke("deals.create", { title: "Explicit" })); assert.equal(requests.length, before + 1); }
1043
+ });
1044
+
1045
+ test("Replicate submits, polls and cancels model predictions without losing media or text outputs", async t => {
1046
+ const entry = cases.find(entry => entry.provider === replicateProvider);
1047
+ const { service, options, input, state, requests } = await fixture(t, entry);
1048
+ await service.connectApiKey(input);
1049
+ const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
1050
+ state.response = { owner: "fixture", name: "model", latest_version: { id: "a".repeat(64), openapi_schema: { components: { schemas: { Input: { properties: { prompt: { type: "string" } } } } } } } };
1051
+ assert.deepEqual(await invoke("models.get", { owner: "fixture", name: "model" }), state.response);
1052
+ assert.equal(requests.at(-1).url.pathname, "/v1/models/fixture/model");
1053
+ state.response = { id: "job1", status: "starting", output: null };
1054
+ const modelInput = { prompt: "A garden", image: "https://files.example.test/input.png", count: 2 };
1055
+ await invoke("predictions.create", { version: "a".repeat(64), input: modelInput });
1056
+ assert.equal(requests.at(-1).url.pathname, "/v1/predictions");
1057
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { version: "a".repeat(64), input: modelInput });
1058
+ await invoke("models.predict", { owner: "fixture", name: "model", input: modelInput });
1059
+ assert.equal(requests.at(-1).url.pathname, "/v1/models/fixture/model/predictions");
1060
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { input: modelInput });
1061
+ for (const output of ["Generated text", ["https://files.example.test/image.png"], "https://files.example.test/video.mp4", { audio: "https://files.example.test/audio.wav" }]) {
1062
+ state.response = { id: "job1", status: "succeeded", output, metrics: { predict_time: 2 } };
1063
+ assert.deepEqual(await invoke("predictions.get", { id: "job1" }), state.response);
1064
+ assert.equal(requests.at(-1).url.pathname, "/v1/predictions/job1");
1065
+ }
1066
+ for (const status of ["processing", "failed", "canceled"]) {
1067
+ state.response = { id: "job1", status, output: null, error: status === "failed" ? "Model input failed" : null };
1068
+ assert.deepEqual(await invoke(status === "canceled" ? "predictions.cancel" : "predictions.get", { id: "job1" }), state.response);
1069
+ }
1070
+ assert.equal(requests.at(-1).init.method, "POST");
1071
+ assert.equal(requests.at(-1).url.pathname, "/v1/predictions/job1/cancel");
1072
+ const count = requests.length;
1073
+ for (const [op, values] of [["predictions.get", { id: "../other" }], ["models.predict", { owner: "fixture", name: "model", input: { prompt: "x".repeat(262145) } }], ["predictions.create", { version: "unversioned", input: {} }]])
1074
+ await assert.rejects(invoke(op, values), { code: "connector_input_invalid" });
1075
+ const denied = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "predictions.cancel" ? null : owner });
1076
+ await assert.rejects(denied.invoke({ ...input, operation: "predictions.cancel", input: { id: "job1" } }), { code: "connector_access_denied" });
1077
+ assert.equal(requests.length, count);
1078
+ for (const status of [403, 429, 500]) { state.status = status; const before = requests.length; await assert.rejects(invoke("models.predict", { owner: "fixture", name: "model", input: {} })); assert.equal(requests.length, before + 1); }
1079
+ });