@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,278 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
7
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
8
+ import { parseIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
9
+ import { clickhouseProvider } from "../src/server/clickhouse.js";
10
+
11
+ const context = { applicationId: "analytics-app", subjectId: "owner" };
12
+ const input = { context, integrationId: "warehouse" };
13
+ const checked = { meta: [{ name: "ok", type: "UInt8" }, { name: "user", type: "String" }], data: [{ ok: 1, user: "reader" }], rows: 1 };
14
+
15
+ async function fixture(t, authentication = { method: "api-key", secretRef: "env:CLICKHOUSE_PASSWORD" }) {
16
+ const directory = await mkdtemp(path.join(tmpdir(), "clickhouse-connector-"));
17
+ t.after(() => rm(directory, { recursive: true, force: true }));
18
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(5) }, activeKeyId: "current" });
19
+ const requests = [];
20
+ const resolutions = [];
21
+ const state = { secret: "fixture:pässword", response: checked, status: 200, text: undefined };
22
+ const options = {
23
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { warehouse: {
24
+ provider: "clickhouse", displayName: "Reports", accountMode: "shared", scopes: [], authentication,
25
+ settings: { httpUrl: "https://warehouse.example:8443/query/", ...(authentication.method === "api-key" ? { username: "reader" } : {}) }
26
+ } }, extensions: { fromCli: true } },
27
+ providers: [clickhouseProvider], authorize: async (owner) => owner,
28
+ resolveReference: async (reference) => { resolutions.push(reference); return state.secret; },
29
+ store: createFileConnectionStore({ directory, protection }),
30
+ fetchImpl: async (address, init) => {
31
+ requests.push({ url: new URL(String(address)), init, headers: new Headers(init.headers) });
32
+ return state.text === undefined ? Response.json(state.response, { status: state.status }) : new Response(state.text, { status: state.status });
33
+ }
34
+ };
35
+ return { service: createConnectionService(options), options, directory, protection, requests, resolutions, state };
36
+ }
37
+
38
+ test("ClickHouse verifies Basic credentials without putting them in URLs or files, and resolves rotation after restart", async (t) => {
39
+ const { service, options, directory, protection, requests, resolutions, state } = await fixture(t);
40
+ const connected = await service.connectApiKey(input);
41
+ assert.equal(connected.status, "connected");
42
+ const { url, init, headers } = requests[0];
43
+ assert.equal(url.origin, "https://warehouse.example:8443");
44
+ assert.equal(url.pathname, "/query/");
45
+ assert.equal(headers.get("authorization"), `Basic ${Buffer.from(`reader:${state.secret}`).toString("base64")}`);
46
+ assert.equal(init.redirect, "error");
47
+ assert.equal(init.credentials, "omit");
48
+ assert.equal(headers.has("cookie"), false);
49
+ assert.deepEqual(Object.fromEntries(url.searchParams), {
50
+ query: "SELECT 1 AS ok, currentUser() AS user FORMAT JSON", readonly: "1", wait_end_of_query: "1",
51
+ max_execution_time: "10", max_result_rows: "100", max_result_bytes: "5242880", result_overflow_mode: "throw"
52
+ });
53
+ for (const name of await readdir(directory)) {
54
+ const text = await readFile(path.join(directory, name), "utf8");
55
+ assert.equal(text.includes(state.secret), false);
56
+ assert.equal(text.includes(headers.get("authorization")), false);
57
+ }
58
+ assert.equal(JSON.stringify(connected).includes(state.secret), false);
59
+ assert.deepEqual(resolutions, ["env:CLICKHOUSE_PASSWORD"]);
60
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
61
+ assert.deepEqual(await restarted.status(input), connected);
62
+ assert.equal(requests.length, 1, "status validates the local credential binding without querying the database");
63
+ assert.deepEqual(resolutions, ["env:CLICKHOUSE_PASSWORD", "env:CLICKHOUSE_PASSWORD"]);
64
+ state.secret = "rotated-password";
65
+ await restarted.invoke({ ...input, operation: "connection.check" });
66
+ assert.equal(requests.at(-1).headers.get("authorization"), `Basic ${Buffer.from("reader:rotated-password").toString("base64")}`);
67
+ assert.deepEqual(resolutions, ["env:CLICKHOUSE_PASSWORD", "env:CLICKHOUSE_PASSWORD", "env:CLICKHOUSE_PASSWORD"]);
68
+ await restarted.disconnect(input);
69
+ assert.equal((await service.status(input)).status, "disconnected");
70
+ });
71
+
72
+ test("ClickHouse distinguishes an empty password, a missing explicit binding and requests with no credentials", async (t) => {
73
+ const { options, requests, resolutions, state } = await fixture(t, { method: "api-key" });
74
+ delete options.configuration.integrations.warehouse.settings.username;
75
+ const empty = createConnectionService(options);
76
+ await empty.connectApiKey(input);
77
+ assert.equal(requests[0].headers.get("authorization"), `Basic ${Buffer.from("default:").toString("base64")}`);
78
+ assert.equal(resolutions.length, 0);
79
+ options.configuration.integrations.warehouse.authentication.secretRef = "env:EMPTY_PASSWORD";
80
+ state.secret = "";
81
+ const explicit = createConnectionService(options);
82
+ await explicit.connectApiKey(input);
83
+ assert.equal(requests.at(-1).headers.get("authorization"), requests[0].headers.get("authorization"));
84
+ for (const value of [undefined, null, 12, "password\nheader"]) {
85
+ state.secret = value;
86
+ await assert.rejects(explicit.invoke({ ...input, operation: "connection.check" }), { code: "connector_binding_missing" });
87
+ }
88
+ assert.equal(requests.length, 2, "An invalid explicit binding must never fall back to an empty password.");
89
+ options.configuration.integrations.warehouse.authentication = { method: "none" };
90
+ const noCredentials = createConnectionService(options);
91
+ await noCredentials.connectWithoutCredentials(input);
92
+ await noCredentials.invoke({ ...input, operation: "connection.check" });
93
+ assert.ok(requests.slice(2).every(({ headers, url }) => !headers.has("authorization") && !url.searchParams.has("password") && !url.searchParams.has("user")));
94
+ assert.equal(resolutions.length, 5);
95
+ });
96
+
97
+ test("ClickHouse uses typed SQL parameters for discovery and rows and preserves returned data types", async (t) => {
98
+ const { service, requests, state } = await fixture(t);
99
+ await service.connectApiKey(input);
100
+ state.response = { meta: [{ name: "name", type: "String" }], data: [{ database: "reports", name: "orders", engine: "MergeTree" }], rows: 1 };
101
+ await service.invoke({ ...input, operation: "tables.list", input: { database: "a' OR 1=1 --\\N", limit: 100, offset: 50 } });
102
+ let params = requests.at(-1).url.searchParams;
103
+ assert.equal(params.get("query"), "SELECT database, name, engine FROM system.tables WHERE database = {database:String} ORDER BY database, name LIMIT {limit:UInt32} OFFSET {offset:UInt64} FORMAT JSON");
104
+ assert.equal(params.get("param_database"), "a' OR 1=1 --\\\\N", "Escape backslashes for ClickHouse's escaped HTTP parameter format.");
105
+ assert.equal(params.get("param_limit"), "100");
106
+ assert.equal(params.get("param_offset"), "50");
107
+ await service.invoke({ ...input, operation: "tables.list" });
108
+ assert.equal(requests.at(-1).url.searchParams.get("query").includes(" WHERE "), false);
109
+ state.response = { meta: [{ name: "name", type: "String" }], data: [{ name: "total", type: "Decimal(18, 2)", position: "2", default_kind: "", default_expression: "" }], rows: 1 };
110
+ await service.invoke({ ...input, operation: "columns.list", input: { database: "reports", table: "order.items" } });
111
+ assert.equal(requests.at(-1).url.searchParams.get("param_table"), "order.items");
112
+ assert.match(requests.at(-1).url.searchParams.get("query"), /database = \{database:String\} AND table = \{table:String\}/u);
113
+ state.response = { meta: [{ name: "id", type: "UInt64" }], data: [{ id: "18446744073709551615", total: "20.00", extra: null, tags: ["one"] }], rows: 1 };
114
+ const response = await service.invoke({ ...input, operation: "rows.list", input: { database: "db.name", table: "table`name", orderBy: "id; DROP TABLE x" } });
115
+ assert.deepEqual(response, state.response);
116
+ params = requests.at(-1).url.searchParams;
117
+ assert.equal(params.get("query"), "SELECT * FROM {database:Identifier}.{table:Identifier} ORDER BY {orderBy:Identifier} LIMIT {limit:UInt32} OFFSET {offset:UInt64} FORMAT JSON");
118
+ assert.equal(params.get("param_orderBy"), "id; DROP TABLE x");
119
+ assert.equal(params.get("param_database"), "db.name");
120
+ assert.equal(params.get("param_table"), "table`name");
121
+ assert.equal(params.get("param_limit"), "20");
122
+ assert.equal(params.get("param_offset"), "0");
123
+ assert.ok(requests.every(({ init }) => init.method === "GET" && init.body === undefined));
124
+ });
125
+
126
+ test("ClickHouse rejects invalid settings, credential references, SQL and query-setting overrides before HTTP", async (t) => {
127
+ const { service, options, requests } = await fixture(t);
128
+ const parse = (configuration) => parseIntegrationConfiguration(JSON.stringify(configuration), { providers: [clickhouseProvider] });
129
+ assert.deepEqual(parse(options.configuration), options.configuration);
130
+ for (const httpUrl of [undefined, "", "http://warehouse.example", "https://user:pass@warehouse.example", "https://warehouse.example?readonly=0", "https://warehouse.example/#fragment", "https://warehouse.example/a/../"]) {
131
+ const configuration = structuredClone(options.configuration);
132
+ configuration.integrations.warehouse.settings.httpUrl = httpUrl;
133
+ assert.throws(() => parse(configuration), (error) => Boolean(error.fieldErrors["integrations.warehouse.settings.httpUrl"]));
134
+ }
135
+ for (const username of ["", "user:password", "bad\nuser"]) {
136
+ const configuration = structuredClone(options.configuration);
137
+ configuration.integrations.warehouse.settings.username = username;
138
+ assert.throws(() => parse(configuration), (error) => Boolean(error.fieldErrors["integrations.warehouse.settings.username"]));
139
+ }
140
+ const configuration = structuredClone(options.configuration);
141
+ configuration.integrations.warehouse.authentication.secretRef = "raw-password";
142
+ assert.throws(() => parse(configuration), (error) => Boolean(error.fieldErrors["integrations.warehouse.authentication.secretRef"]));
143
+ await service.connectApiKey(input);
144
+ for (const value of [{}, { database: "reports" }, { database: "", table: "x" }, { database: "x", table: "bad\tname" }]) {
145
+ await assert.rejects(service.invoke({ ...input, operation: "rows.list", input: value }), { code: "connector_input_invalid" });
146
+ }
147
+ for (const value of [{ limit: 101 }, { limit: 0 }, { limit: 1.5 }, { offset: -1 }, { offset: 1000001 }, { query: "DROP TABLE x" }, { readonly: 0 }, { url: "https://other.example" }, { max_execution_time: 0 }, { settings: {} }]) {
148
+ await assert.rejects(service.invoke({ ...input, operation: "tables.list", input: value }), { code: "connector_input_invalid" });
149
+ }
150
+ assert.equal(requests.length, 1);
151
+ });
152
+
153
+ test("ClickHouse accepts empty results and rejects partial or error results, including HTTP-200 exceptions", async (t) => {
154
+ const { service, requests, state } = await fixture(t);
155
+ state.response = { meta: [], data: [], rows: 0 };
156
+ await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
157
+ assert.equal((await service.status(input)).status, "disconnected");
158
+ state.response = checked;
159
+ await service.connectApiKey(input);
160
+ for (const operation of ["tables.list", "columns.list", "rows.list"]) {
161
+ state.response = { meta: [], data: [], rows: 0 };
162
+ const fields = operation === "tables.list" ? {} : { database: "reports", table: "orders" };
163
+ assert.deepEqual(await service.invoke({ ...input, operation, input: fields }), state.response);
164
+ }
165
+ for (const response of [{}, { meta: [], data: [{}], rows: 0 }, { meta: [{}], data: [], rows: 0 },
166
+ { meta: [], data: [null], rows: 1 }, { meta: [], data: Array(101).fill({}), rows: 101 },
167
+ { meta: [], data: [], rows: 0, exception: state.secret }, { error: state.secret }]) {
168
+ state.response = response;
169
+ await assert.rejects(service.invoke({ ...input, operation: "rows.list", input: { database: "reports", table: "orders" } }), { code: "connector_response_invalid" });
170
+ }
171
+ for (const text of ["{\"meta\":[],\"data\":[", `${JSON.stringify(checked)}\nCode: 241. DB::Exception: ${state.secret}`]) {
172
+ state.text = text;
173
+ const count = requests.length;
174
+ await assert.rejects(service.invoke({ ...input, operation: "connection.check" }), (error) => {
175
+ assert.equal(error.code, "connector_response_invalid");
176
+ assert.equal(error.stack.includes(state.secret), false);
177
+ return true;
178
+ });
179
+ assert.equal(requests.length, count + 1);
180
+ }
181
+ });
182
+
183
+ test("ClickHouse isolates connections and requires verification after changing mode, username or destination", async (t) => {
184
+ const { service, options, requests, resolutions } = await fixture(t);
185
+ await service.connectApiKey(input);
186
+ for (const owner of [{ ...context, applicationId: "other" }, { ...context, subjectId: "other" }]) {
187
+ await assert.rejects(service.invoke({ ...input, context: owner, operation: "connection.check" }), { code: "connector_reconnect_required" });
188
+ }
189
+ for (const changes of [{ username: "other" }, { httpUrl: "https://other.example" }, { httpUrl: "https://warehouse.example:8443/other/" }]) {
190
+ const configuration = structuredClone(options.configuration);
191
+ Object.assign(configuration.integrations.warehouse.settings, changes);
192
+ const changed = createConnectionService({ ...options, configuration });
193
+ assert.equal((await changed.status(input)).status, "reconnect-required");
194
+ await assert.rejects(changed.invoke({ ...input, operation: "connection.check" }), { code: "connector_reconnect_required" });
195
+ }
196
+ const denied = createConnectionService({ ...options, authorize: async () => { throw new Error("Access denied"); } });
197
+ await assert.rejects(denied.connectApiKey(input), /Access denied/u);
198
+ assert.equal(requests.length, 1);
199
+ assert.equal(resolutions.length, 1);
200
+ const configuration = structuredClone(options.configuration);
201
+ configuration.integrations.warehouse.authentication = { method: "none" };
202
+ delete configuration.integrations.warehouse.settings.username;
203
+ const changed = createConnectionService({ ...options, configuration });
204
+ await assert.rejects(changed.invoke({ ...input, operation: "connection.check" }), { code: "connector_reconnect_required" });
205
+ await changed.connectWithoutCredentials(input);
206
+ assert.equal((await service.status(input)).status, "reconnect-required");
207
+ assert.equal(requests.at(-1).headers.has("authorization"), false);
208
+ });
209
+
210
+ test("ClickHouse keeps provider permission, quota and credential failures observable without claiming table access", async (t) => {
211
+ const { service, requests, state } = await fixture(t);
212
+ for (const [status, code] of [[401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
213
+ state.status = status;
214
+ state.response = { error: state.secret };
215
+ const count = requests.length;
216
+ await assert.rejects(service.connectApiKey(input), (error) => {
217
+ assert.equal(error.code, code);
218
+ assert.equal(error.stack.includes(state.secret), false);
219
+ return true;
220
+ });
221
+ assert.equal((await service.status(input)).status, "disconnected");
222
+ assert.equal(requests.length, count + 1);
223
+ }
224
+ state.status = 200;
225
+ state.response = checked;
226
+ await service.connectApiKey(input);
227
+ state.status = 403;
228
+ await assert.rejects(service.invoke({ ...input, operation: "rows.list", input: { database: "private", table: "orders" } }), { code: "connector_permission_denied" });
229
+ assert.equal((await service.status(input)).status, "connected");
230
+ state.status = 401;
231
+ await assert.rejects(service.invoke({ ...input, operation: "connection.check" }), { code: "connector_reconnect_required" });
232
+ assert.equal((await service.status(input)).status, "reconnect-required");
233
+ });
234
+
235
+ test("ClickHouse reports timeout and cancellation without replaying a database query", async (t) => {
236
+ const { service, options } = await fixture(t);
237
+ await service.connectApiKey(input);
238
+ for (const [failure, code] of [["TimeoutError", "connector_provider_timeout"], ["AbortError", "connector_cancelled"]]) {
239
+ let calls = 0;
240
+ const controller = new AbortController();
241
+ const interrupted = createConnectionService({ ...options, fetchImpl: async () => {
242
+ calls++;
243
+ controller.abort(new DOMException("Interrupted", failure));
244
+ throw new Error("Wrapped transport failure");
245
+ } });
246
+ await assert.rejects(interrupted.invoke({ ...input, operation: "connection.check", signal: controller.signal }), { code });
247
+ assert.equal(calls, 1);
248
+ }
249
+ });
250
+
251
+
252
+ test("ClickHouse analytical queries keep typed values separate, bounded and authorized in both modes", async t => {
253
+ for (const authentication of [{ method: "api-key", secretRef: "env:CLICKHOUSE_PASSWORD" }, { method: "none" }]) {
254
+ const { service, options, state, requests } = await fixture(t, authentication);
255
+ if (authentication.method === "none") await service.connectWithoutCredentials(input); else await service.connectApiKey(input);
256
+ const sql = "SELECT toStartOfMonth(booked_at) AS month, count() AS bookings FROM {db:Identifier}.{table:Identifier} WHERE tenant = {tenant:String} GROUP BY month ORDER BY month LIMIT 100";
257
+ const values = { sql, parameters: { db: "reports", table: "bookings", tenant: "O'Reilly\\shop" } };
258
+ state.response = { meta: [{ name: "bookings", type: "UInt64" }], data: [{ bookings: "9007199254740993" }], rows: 1, statistics: { rows_read: 10000 } };
259
+ assert.deepEqual(await service.invoke({ ...input, operation: "queries.read", input: values }), state.response);
260
+ const request = requests.at(-1);
261
+ assert.equal(request.init.method, "GET"); assert.equal(request.url.searchParams.get("query"), `${sql} FORMAT JSON`);
262
+ assert.equal(request.url.searchParams.get("param_tenant"), values.parameters.tenant.replaceAll("\\", "\\\\"));
263
+ assert.equal(request.url.searchParams.get("readonly"), "1"); assert.equal(request.url.searchParams.get("max_result_rows"), "100");
264
+ assert.equal(request.url.searchParams.has("param_sql"), false); assert.equal(request.headers.has("authorization"), authentication.method === "api-key");
265
+ const count = requests.length;
266
+ for (const invalid of [{ sql: "DROP TABLE bookings" }, { sql: "SELECT 1; SELECT 2" }, { sql: "SELECT 1", readonly: 0 },
267
+ { sql: "SELECT {x:Int32}", parameters: { x: 3 } }, { sql: "SELECT 1", parameters: { "a&readonly": "0" } }])
268
+ await assert.rejects(service.invoke({ ...input, operation: "queries.read", input: invalid }), { code: "connector_input_invalid" });
269
+ const denied = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "queries.read" ? null : owner });
270
+ await assert.rejects(denied.invoke({ ...input, operation: "queries.read", input: values }), { code: "connector_access_denied" });
271
+ assert.equal(requests.length, count);
272
+ state.status = 500; state.response = { error: "query setting forbidden" };
273
+ await assert.rejects(service.invoke({ ...input, operation: "queries.read", input: values }), { code: "connector_provider_failed" });
274
+ assert.equal(requests.length, count + 1);
275
+ state.status = 200; state.response = { meta: [], data: [], rows: 0, exception: "query interrupted" };
276
+ await assert.rejects(service.invoke({ ...input, operation: "queries.read", input: values }), { code: "connector_response_invalid" });
277
+ }
278
+ });
@@ -0,0 +1,205 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
7
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
8
+ import { parseIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
9
+ import { confidenceFlagsProvider, confidenceExpProvider, registerConfidenceClient } from "../src/server/confidence.js";
10
+
11
+ const scopes = ["openid", "profile", "email", "offline_access"];
12
+ const context = { applicationId: "assistant", subjectId: "owner" };
13
+ const input = { context, integrationId: "research" };
14
+ const callback = "http://127.0.0.1:4930/confidence/callback";
15
+ const origin = "https://mcp.confidence.dev";
16
+ const specs = [
17
+ { provider: confidenceFlagsProvider, path: "flags", name: "getFlag", arguments: { name: "flags/approved-flag" } },
18
+ { provider: confidenceExpProvider, path: "experiments", name: "get_experiment", arguments: { name: "workflows/abtest/instances/approved" } }
19
+ ];
20
+ async function fixture(t, spec) {
21
+ const directory = await mkdtemp(path.join(tmpdir(), "confidence-connector-"));
22
+ t.after(() => rm(directory, { recursive: true, force: true }));
23
+ const requests = [];
24
+ const state = { time: Date.now(), scopes, status: 200,
25
+ list: { tools: [{ name: spec.name, inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } }] },
26
+ call: { content: [{ type: "text", text: "Fixture resource" }] } };
27
+ const options = {
28
+ configuration: { schemaVersion: 1, registrations: { confidence: { source: "own", clientId: "client", clientSecretRef: "env:CONFIDENCE_SECRET", callbackUrlRef: "env:CONFIDENCE_CALLBACK" } },
29
+ integrations: { research: { provider: spec.provider.id, accountMode: "assistant", scopes, authentication: { method: "oauth2", registrationRef: "confidence" } } } },
30
+ providers: specs.map((item) => item.provider), now: () => state.time,
31
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(8) }, activeKeyId: "current" }) }),
32
+ authorize: async (owner, request) => request.operation === "tools.call" && (request.input.name !== spec.name || request.input.arguments.name !== spec.arguments.name) ? null : owner,
33
+ resolveReference: async (reference) => reference === "env:CONFIDENCE_SECRET" ? "private-client-secret" : callback,
34
+ fetchImpl: async (address, init) => {
35
+ init.signal.throwIfAborted(); const url = String(address); requests.push({ url, init, headers: new Headers(init.headers) });
36
+ if (url === `${origin}/token`) {
37
+ if (state.tokenError) return Response.json({ error: state.tokenError, error_description: "private-client-secret" }, { status: 400 });
38
+ const refresh = new URLSearchParams(init.body).get("grant_type") === "refresh_token";
39
+ return Response.json({ token_type: "Bearer", access_token: refresh ? "access-two" : "access-one", refresh_token: refresh ? "refresh-two" : "refresh-one", expires_in: 60, scope: state.scopes.join(" ") });
40
+ }
41
+ assert.equal(url, `${origin}/mcp/${spec.path}`);
42
+ if (init.method === "DELETE") return new Response(null, { status: 204 });
43
+ if (init.method === "GET") return new Response(null, { status: 405 });
44
+ if (state.status !== 200) return new Response("private-client-secret access-one", { status: state.status,
45
+ headers: { "WWW-Authenticate": 'Bearer resource_metadata="http://mcp.confidence.dev/.well-known/oauth-protected-resource/mcp"' } });
46
+ const message = JSON.parse(init.body); requests.at(-1).message = message;
47
+ if (message.id === undefined) return new Response(null, { status: 202 });
48
+ if (state.pause && message.method === "tools/call") {
49
+ state.started(); return new Promise((_, reject) => init.signal.addEventListener("abort", () => reject(init.signal.reason), { once: true }));
50
+ }
51
+ const result = message.method === "initialize" ? { protocolVersion: message.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "confidence-fixture", version: "1" } }
52
+ : message.method === "tools/list" ? state.list : state.call;
53
+ return Response.json({ jsonrpc: "2.0", id: message.id, result }, { headers: { "Mcp-Session-Id": "fixture-session" } });
54
+ }
55
+ };
56
+ const service = createConnectionService(options);
57
+ const complete = (start, extra = "code=fixture-code") => service.completeAuthorization({ ...input, callbackUrl: `${callback}?state=${new URL(start.authorizationUrl).searchParams.get("state")}&${extra}` });
58
+ return { service, options, state, requests, directory, complete };
59
+ }
60
+
61
+ for (const spec of specs) {
62
+ test(`${spec.provider.name} binds PKCE to the declared common resource and preserves encrypted grants across refresh and restart`, async (t) => {
63
+ const f = await fixture(t, spec); const start = await f.service.beginAuthorization(input); const url = new URL(start.authorizationUrl);
64
+ assert.equal(url.origin + url.pathname, `${origin}/authorize`); assert.equal(url.searchParams.get("resource"), `${origin}/mcp`);
65
+ assert.equal(url.searchParams.get("code_challenge_method"), "S256"); assert.equal(url.searchParams.get("scope"), scopes.join(" "));
66
+ assert.equal((await f.complete(start)).status, "connected");
67
+ const body = new URLSearchParams(f.requests.find((request) => request.url.endsWith("/token")).init.body);
68
+ assert.equal(body.get("resource"), `${origin}/mcp`); assert.equal(body.get("client_secret"), "private-client-secret"); assert.equal(body.get("redirect_uri"), callback); assert(body.get("code_verifier"));
69
+ assert.equal(f.requests.some(({ message }) => message?.method === "tools/call"), false);
70
+ const restarted = createConnectionService(f.options); f.state.time += 61_000;
71
+ f.state.list = { tools: [], nextCursor: "next-page" };
72
+ assert.deepEqual(await restarted.invoke({ ...input, operation: "tools.list", input: { cursor: "next-page" } }), f.state.list);
73
+ assert.deepEqual(f.requests.findLast(({ message }) => message?.method === "tools/list").message.params, { cursor: "next-page" });
74
+ const refresh = new URLSearchParams(f.requests.findLast(({ url }) => url.endsWith("/token")).init.body);
75
+ assert.equal(refresh.get("resource"), `${origin}/mcp`); assert.equal(refresh.get("refresh_token"), "refresh-one");
76
+ assert.equal(f.requests.at(-1).headers.get("authorization"), "Bearer access-two");
77
+ for (const request of f.requests.filter(({ url }) => url.includes("/mcp/"))) { assert.equal(request.init.redirect, "error"); assert.equal(request.init.credentials, "omit"); }
78
+ for (const file of await readdir(f.directory)) for (const secret of ["private-client-secret", "access-one", "access-two", "refresh-one", "refresh-two", body.get("code_verifier")]) assert(!(await readFile(path.join(f.directory, file), "utf8")).includes(secret));
79
+ const count = f.requests.length;
80
+ await assert.rejects(f.complete(start), { code: "connector_attempt_invalid" });
81
+ for (const owner of [{ ...context, applicationId: "other" }, { ...context, subjectId: "other" }]) await assert.rejects(restarted.invoke({ ...input, context: owner, operation: "tools.list" }), { code: "connector_reconnect_required" });
82
+ await restarted.disconnect(input); assert.equal((await f.service.status(input)).status, "disconnected"); assert.equal(f.requests.length, count);
83
+ });
84
+
85
+ test(`${spec.provider.name} authorizes exact tool resources, preserves errors and cancels without replay`, async (t) => {
86
+ const f = await fixture(t, spec); await f.complete(await f.service.beginAuthorization(input)); const before = f.requests.length;
87
+ for (const call of [{ name: "deleteEverything", arguments: {} }, { name: spec.name, arguments: { name: "another-owner-resource" } }]) await assert.rejects(f.service.invoke({ ...input, operation: "tools.call", input: call }), { code: "connector_access_denied" });
88
+ await assert.rejects(f.service.invoke({ ...input, operation: "tools.list", input: { url: "https://invalid.test" } }), { code: "connector_input_invalid" });
89
+ assert.equal(f.requests.length, before);
90
+ const call = { name: spec.name, arguments: spec.arguments }; f.state.call = { content: [{ type: "text", text: "Fixture tool failure" }], isError: true };
91
+ assert.deepEqual(await f.service.invoke({ ...input, operation: "tools.call", input: call }), f.state.call);
92
+ assert.deepEqual(f.requests.findLast(({ message }) => message?.method === "tools/call").message.params, call);
93
+ f.state.pause = true; const started = new Promise((resolve) => { f.state.started = resolve; }); const controller = new AbortController();
94
+ const pending = assert.rejects(f.service.invoke({ ...input, operation: "tools.call", input: call, signal: controller.signal }), { code: "connector_cancelled" });
95
+ await started; controller.abort(); await pending;
96
+ assert.equal(f.requests.filter(({ message }) => message?.method === "tools/call").length, 2); assert.equal(f.requests.at(-1).init.method, "DELETE");
97
+ });
98
+
99
+ test(`${spec.provider.name} rejects declined or reduced consent and redacts transport errors without HTTP discovery`, async (t) => {
100
+ const f = await fixture(t, spec);
101
+ await assert.rejects(f.complete(await f.service.beginAuthorization(input), "error=access_denied"), { code: "connector_consent_denied" }); assert.equal(f.requests.length, 0);
102
+ f.state.scopes = ["profile"]; await assert.rejects(f.complete(await f.service.beginAuthorization(input)), { code: "connector_scope_missing" });
103
+ f.state.scopes = scopes; f.state.list = { wrong: [] }; await assert.rejects(f.complete(await f.service.beginAuthorization(input)), { code: "connector_provider_failed" });
104
+ f.state.list = { tools: [] };
105
+ for (const [status, code] of [[401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
106
+ f.state.status = status;
107
+ await assert.rejects(f.complete(await f.service.beginAuthorization(input)), (error) => error.code === code && error.cause === undefined && !error.message.includes("private-client-secret"));
108
+ }
109
+ assert(f.requests.every(({ url }) => url === `${origin}/token` || url === `${origin}/mcp/${spec.path}`));
110
+ f.state.status = 200; await f.complete(await f.service.beginAuthorization(input)); f.state.time += 61_000; f.state.tokenError = "invalid_grant";
111
+ await assert.rejects(f.service.invoke({ ...input, operation: "tools.list" }), { code: "connector_reconnect_required" });
112
+ assert.equal((await f.service.status(input)).status, "reconnect-required");
113
+ });
114
+ }
115
+
116
+ test("Confidence Flags and Experiments cannot reuse a pending attempt or connected grant by changing provider", async (t) => {
117
+ const f = await fixture(t, specs[0]); const start = await f.service.beginAuthorization(input);
118
+ const config = structuredClone(f.options.configuration); config.integrations.research.provider = confidenceExpProvider.id;
119
+ const changed = createConnectionService({ ...f.options, configuration: config });
120
+ await assert.rejects(changed.completeAuthorization({ ...input, callbackUrl: `${callback}?code=x&state=${new URL(start.authorizationUrl).searchParams.get("state")}` }), { code: "connector_attempt_invalid" });
121
+ assert.equal(f.requests.length, 0); await f.complete(await f.service.beginAuthorization(input)); const before = f.requests.length;
122
+ assert.equal((await changed.status(input)).status, "reconnect-required");
123
+ await assert.rejects(changed.invoke({ ...input, operation: "tools.list" }), { code: "connector_reconnect_required" }); assert.equal(f.requests.length, before);
124
+ });
125
+
126
+ test("Confidence registration uses confidential client metadata and exact application-owned HTTPS callbacks", async () => {
127
+ for (const spec of specs) {
128
+ const callbackUrl = `https://application.example/integrations/${spec.provider.id}/callback`; const requests = [];
129
+ const result = await registerConfidenceClient({ clientName: spec.provider.name, callbackUrl, scopes }, { fetchImpl: async (url, init) => {
130
+ requests.push({ url, init }); return Response.json({ ...JSON.parse(init.body), client_id: "created-client", client_secret: "created-secret", client_secret_expires_at: 0 });
131
+ } });
132
+ assert.deepEqual(result, { clientId: "created-client", clientSecret: "created-secret", clientSecretExpiresAt: 0 }); assert.equal(requests.length, 1);
133
+ assert.equal(String(requests[0].url), `${origin}/register`); assert.equal(requests[0].init.redirect, "error"); assert.equal(requests[0].init.credentials, "omit");
134
+ assert.deepEqual(JSON.parse(requests[0].init.body), { client_name: spec.provider.name, redirect_uris: [callbackUrl], token_endpoint_auth_method: "client_secret_post", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], scope: scopes.join(" ") });
135
+ }
136
+ });
137
+
138
+ test("Confidence registration rejects invalid or ambiguous responses and interruption without repeat creation", async () => {
139
+ const valid = { clientName: "Assistant", callbackUrl: callback, scopes }; let requests = 0;
140
+ const options = { fetchImpl: async () => { requests++; throw Error("private response"); } };
141
+ for (const change of [{ scopes: ["flags:read"] }, { scopes: ["openid", "openid"] }, { callbackUrl: "http://remote.test/callback" }, { callbackUrl: "https://user:pass@host.test/callback" }, { clientName: "" }]) await assert.rejects(registerConfidenceClient({ ...valid, ...change }, options));
142
+ assert.equal(requests, 0); await assert.rejects(registerConfidenceClient(valid, options), { code: "connector_registration_failed" }); assert.equal(requests, 1);
143
+ for (const change of [{ client_secret: undefined }, { token_endpoint_auth_method: "none" }, { redirect_uris: ["https://unexpected.test"] }]) {
144
+ await assert.rejects(registerConfidenceClient(valid, { fetchImpl: async (_, init) => Response.json({ ...JSON.parse(init.body), client_id: "client", client_secret: "secret", ...change }) }), { code: "connector_registration_failed" });
145
+ }
146
+ const controller = new AbortController(); controller.abort(); await assert.rejects(registerConfidenceClient(valid, { ...options, signal: controller.signal }), { code: "connector_registration_interrupted" }); assert.equal(requests, 1);
147
+ });
148
+
149
+ test("Both Confidence guides use the portable assistant-only registration contract", async () => {
150
+ for (const { provider } of specs) {
151
+ const text = await readFile(new URL(`../docs/${provider.id}.md`, import.meta.url), "utf8"); const json = text.match(/```json\n([\s\S]*?)\n```/u)[1];
152
+ const config = parseIntegrationConfiguration(json, { providers: [provider] }); const integration = Object.values(config.integrations)[0];
153
+ assert.equal(integration.provider, provider.id); assert.equal(integration.accountMode, "assistant");
154
+ for (const mode of ["shared", "per-user"]) { integration.accountMode = mode; assert.throws(() => parseIntegrationConfiguration(JSON.stringify(config), { providers: [provider] })); }
155
+ }
156
+ });
157
+
158
+
159
+ test("Confidence Exp retains actual analysis content and limits calls to approved experiment resources", async t => {
160
+ const spec = specs.find(item => item.provider === confidenceExpProvider);
161
+ const f = await fixture(t, spec); await f.complete(await f.service.beginAuthorization(input));
162
+ const allowed = createConnectionService({ ...f.options, authorize: async (owner, request) =>
163
+ request.operation !== "tools.call" || ["get_experiment", "get_results"].includes(request.input.name) && request.input.arguments.name === spec.arguments.name ? owner : null });
164
+ for (const [name, result] of [["get_experiment", { name: spec.arguments.name, state: "live", analysisResults: ["results/primary"] }],
165
+ ["get_results", { relativeEffect: 0.04, confidenceInterval: [-0.01, 0.09], significant: false, sampleSizes: { control: 200, treatment: 205 }, recommendation: "continue" }]]) {
166
+ f.state.call = { content: [{ type: "text", text: JSON.stringify(result) }], isError: false };
167
+ const call = { name, arguments: { name: spec.arguments.name, summary: false } };
168
+ assert.deepEqual(await allowed.invoke({ ...input, operation: "tools.call", input: call }), f.state.call);
169
+ assert.deepEqual(f.requests.findLast(request => request.message?.method === "tools/call").message.params, call);
170
+ }
171
+ const count = f.requests.length;
172
+ await assert.rejects(allowed.invoke({ ...input, operation: "tools.call", input: { name: "get_results", arguments: { name: "workflows/abtest/instances/other" } } }), { code: "connector_access_denied" });
173
+ assert.equal(f.requests.length, count);
174
+ });
175
+
176
+
177
+ test("Confidence Flags permits explicitly approved management calls and preserves partial tool failure", async t => {
178
+ const spec = specs.find(item => item.provider === confidenceFlagsProvider);
179
+ const f = await fixture(t, spec); await f.complete(await f.service.beginAuthorization(input));
180
+ // These fixture schemas model discovery, not a claim about every live argument name.
181
+ const calls = [
182
+ { name: "createFlag", arguments: { name: "new-checkout", schema: { type: "boolean" }, variants: [{ name: "off", value: false }] } },
183
+ { name: "addFlagVariant", arguments: { name: "flags/new-checkout", variant: { name: "on", value: true } } },
184
+ { name: "createOverrideRule", arguments: { name: "flags/new-checkout", entity: "test-user", variant: "on" } },
185
+ { name: "testResolveFlag", arguments: { name: "flags/new-checkout", entity: "test-user" } }
186
+ ];
187
+ let approved;
188
+ const host = createConnectionService({ ...f.options, authorize: async (owner, request) =>
189
+ request.operation !== "tools.call" || JSON.stringify(request.input) === JSON.stringify(approved) ? owner : null });
190
+ for (const call of calls) {
191
+ const before = f.requests.length;
192
+ await assert.rejects(host.invoke({ ...input, operation: "tools.call", input: call }), { code: "connector_access_denied" });
193
+ assert.equal(f.requests.length, before);
194
+ approved = call;
195
+ f.state.call = { content: [{ type: "text", text: JSON.stringify({ name: "flags/new-checkout", variant: "on", matchedRule: "test-user-only" }) }], isError: false };
196
+ assert.deepEqual(await host.invoke({ ...input, operation: "tools.call", input: call }), f.state.call);
197
+ assert.deepEqual(f.requests.findLast(request => request.message?.method === "tools/call").message.params, call);
198
+ approved = undefined;
199
+ }
200
+ approved = calls[2]; f.state.call = { content: [{ type: "text", text: "Rule was not saved" }], isError: true };
201
+ assert.deepEqual(await host.invoke({ ...input, operation: "tools.call", input: approved }), f.state.call);
202
+ const before = f.requests.length; f.state.status = 500;
203
+ await assert.rejects(host.invoke({ ...input, operation: "tools.call", input: approved }));
204
+ assert.equal(f.requests.length, before + 1, "uncertain flag writes are not replayed");
205
+ });