@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,79 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
7
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
8
+ import { attentionProvider } from "../src/server/attention.js";
9
+
10
+ test("Attention preserves full call content and supports explicit coaching and organization workflows", async t => {
11
+ const directory = await mkdtemp(path.join(tmpdir(), "attention-repair-"));
12
+ t.after(() => rm(directory, { recursive: true, force: true }));
13
+ const requests = [];
14
+ const state = { status: 200, response: { data: [] }, fail: false, deny: false };
15
+ const identity = { context: { applicationId: "coaching-app", subjectId: "workspace" }, integrationId: "calls" };
16
+ const service = createConnectionService({ configuration: { schemaVersion: 1, registrations: {}, integrations: {
17
+ calls: { provider: "attention", accountMode: "shared", scopes: [], authentication: { method: "api-key", secretRef: "env:ATTENTION_API_KEY" } }
18
+ } }, providers: [attentionProvider], authorize: async context => { if (state.deny) throw new Error("forbidden"); return context; },
19
+ resolveReference: async () => "attention-fixture-key",
20
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(3) }, activeKeyId: "current" }) }),
21
+ fetchImpl: async (url, init) => { requests.push({ url: new URL(url), init });
22
+ if (state.fail) throw new Error("uncertain result");
23
+ return state.response === null ? new Response(null, { status: state.status }) : Response.json(state.response, { status: state.status }); }
24
+ });
25
+ const invoke = (operation, input = {}) => service.invoke({ ...identity, operation, input });
26
+ await service.connectApiKey(identity);
27
+ const call = { type: "conversations", id: "call-1", attributes: { title: "Acme discovery", transcriptStatus: "completed",
28
+ transcript: { segments: [{ speaker: "Sam", start: 1.2, text: "We need appointment reminders." }] },
29
+ participants: [{ name: "Sam", email: "sam@example.test" }], scorecardResults: [{ title: "Discovery", summary: { averageScore: 85 } }] } };
30
+ state.response = call;
31
+ assert.deepEqual(await invoke("conversations.get", { id: "call-1" }), call);
32
+ assert.equal(requests.at(-1).url.searchParams.get("detailedTranscript"), "true");
33
+ for (const [operation, input, endpoint] of [
34
+ ["scorecards.list", { page: 2, size: 50 }, "scorecards"],
35
+ ["scorecardItems.list", { id: "score-1" }, "scorecards/score-1/items"],
36
+ ["users.list", { teamUUID: "team-1" }, "organizations/users"],
37
+ ["roles.list", {}, "organizations/roles"], ["teams.list", {}, "organizations/teams"]
38
+ ]) { state.response = { data: [{ uuid: "item-1", title: "Fixture" }], meta: { pageNumber: 2 } };
39
+ assert.deepEqual(await invoke(operation, input), state.response); assert.equal(requests.at(-1).url.pathname, `/v2/${endpoint}`); }
40
+ const scoring = { scorecard_uuid: "score-1", conversation_uuid: "call-1", summary: "Good discovery",
41
+ items: [{ scorecard_item_uuid: "criterion-1", numeric_result: 85, description: "Established needs" }] };
42
+ for (const [operation, method, endpoint, input, response] of [
43
+ ["conversations.update", "PUT", "conversations/call-1", { id: "call-1", title: "Reviewed call", labels: { reviewed: true } }, call],
44
+ ["conversations.archive", "DELETE", "conversations/call-1", { id: "call-1" }, { ...call, attributes: { ...call.attributes, archived: true } }],
45
+ ["conversations.import", "POST", "conversations/import", { userID: "user-1", mediaURL: "https://media.example.test/call.mp4", skipOpportunitiesExport: true }, { uuid: "import-1" }],
46
+ ["snippets.create", "POST", "snippets", { user_uuid: "user-1", conversation_id: "call-1", internal: true, notify_views: false, video: { start_time: 10, end_time: 20 } }, { id: "clip-1", url: "https://app.attention.tech/clip-1" }],
47
+ ["analysis.ask", "POST", "ask_attention/v2", { conversations_ids: ["call-1"], deal_id: "", prompt: "What matters?" }, [{ output: "Appointment reminders", error: "", conversation_id: "call-1" }]],
48
+ ["scorecards.get", "GET", "scorecards/score-1", { id: "score-1" }, { data: { id: "score-1", title: "Discovery" } }],
49
+ ["scorecardResults.create", "POST", "createScorecardResult", scoring, { success: true }],
50
+ ["scorecards.summary", "POST", "scorecards/summary", { scorecardUUID: "score-1", scorecardsItemsUUIDs: ["criterion-1"], teamUUIDs: [], userUUIDs: ["user-1"], fromDateTime: "2026-09-01T00:00:00Z", toDateTime: "2026-09-12T00:00:00Z" }, { data: [{ averageScore: 85 }] }],
51
+ ["users.create", "POST", "organizations/users", { email: "sam@example.test", first_name: "Sam", last_name: "Test", roleUUID: "role-1", teams: [{ uuid: "team-1", primary: true }], seat_type: "listener" }, { data: { uuid: "user-1" } }],
52
+ ["users.update", "PATCH", "organizations/users/user-1", { id: "user-1", firstName: "Samuel", teamUUIDsToRemove: ["old-team"], teamsToAdd: [{ uuid: "team-1", primary: true }] }, { data: { uuid: "user-1" } }],
53
+ ["users.delete", "DELETE", "organizations/users/user-1", { id: "user-1" }, null],
54
+ ["teams.create", "POST", "organizations/teams", { name: "Coaching", parentTeamUUID: "parent-1" }, { data: { uuid: "team-1" } }],
55
+ ["teams.update", "PATCH", "organizations/teams/team-1", { id: "team-1", name: "Sales coaching" }, { data: { uuid: "team-1" } }]
56
+ ]) { state.response = response; assert.deepEqual(await invoke(operation, input).catch(error => { error.message = `${operation}: ${error.message}`; throw error; }), response === null ? {} : response);
57
+ const request = requests.at(-1); assert.equal(request.url.pathname, `/v2/${endpoint}`); assert.equal(request.init.method, method);
58
+ if (!["GET", "DELETE"].includes(method)) { const { id, ...body } = input; assert.deepEqual(JSON.parse(request.init.body), body); }
59
+ assert.equal(new Headers(request.init.headers).get("authorization"), "Bearer attention-fixture-key"); assert.equal(request.init.redirect, "error");
60
+ }
61
+ const before = requests.length;
62
+ for (const [operation, input] of [
63
+ ["conversations.get", { id: "../../other" }], ["conversations.update", { id: "call-1" }],
64
+ ["conversations.import", { mediaURL: "http://localhost/file", userID: "user-1" }],
65
+ ["snippets.create", { user_uuid: "user-1", conversation_id: "call-1", internal: true, notify_views: false, video: { start_time: 20, end_time: 10 } }],
66
+ ["scorecardResults.create", { ...scoring, items: [{ description: "Missing criterion" }] }],
67
+ ["users.create", { email: "sam@example.test", first_name: "Sam", last_name: "Test", roleUUID: "role-1", teams: [] }],
68
+ ["teams.update", { id: "team-1" }]
69
+ ]) await assert.rejects(invoke(operation, input));
70
+ state.deny = true; await assert.rejects(invoke("users.delete", { id: "user-1" })); state.deny = false;
71
+ assert.equal(requests.length, before);
72
+ state.response = { wrong: true }; await assert.rejects(invoke("conversations.get", { id: "call-1" }), { code: "connector_response_invalid" });
73
+ state.response = { success: false }; await assert.rejects(invoke("scorecardResults.create", scoring), { code: "connector_response_invalid" });
74
+ state.response = [{ conversation_id: "call-1", output: "", error: "Transcript pending" }];
75
+ assert.equal((await invoke("analysis.ask", { conversations_ids: ["call-1"], deal_id: "", prompt: "Summarize" }))[0].error, "Transcript pending");
76
+ for (const status of [403, 429]) { state.status = status; await assert.rejects(invoke("teams.list")); }
77
+ state.status = 200; state.fail = true; const uncertain = requests.length;
78
+ await assert.rejects(invoke("scorecardResults.create", scoring)); assert.equal(requests.length, uncertain + 1);
79
+ });
@@ -0,0 +1,405 @@
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 { awsS3Provider } from "../src/server/aws-s3.js";
10
+ import { awsAthenaProvider } from "../src/server/aws-athena.js";
11
+ import { transferS3Object } from "../patterns/aws-storage-queries/example/s3-transfer.js";
12
+
13
+ const context = { applicationId: "reports", subjectId: "owner" };
14
+ const input = { context, integrationId: "aws" };
15
+ const listXml = '<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Name>app-files</Name><KeyCount>0</KeyCount><MaxKeys>100</MaxKeys><IsTruncated>false</IsTruncated></ListBucketResult>';
16
+ const workgroup = { WorkGroup: { Name: "primary", State: "ENABLED", Configuration: { EnforceWorkGroupConfiguration: true } } };
17
+ const query = { QueryExecution: { QueryExecutionId: "query-1", WorkGroup: "primary", Status: { State: "SUCCEEDED" } } };
18
+ const results = { ResultSet: { ResultSetMetadata: { ColumnInfo: [{ Name: "id", Type: "bigint" }] }, Rows: [{ Data: [{ VarCharValue: "id" }] }, { Data: [{}] }] }, NextToken: "opaque +/=", UpdateCount: 0 };
19
+
20
+ async function fixture(t, provider = awsS3Provider) {
21
+ const directory = await mkdtemp(path.join(tmpdir(), "aws-connector-"));
22
+ t.after(() => rm(directory, { recursive: true, force: true }));
23
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(5) }, activeKeyId: "current" });
24
+ const requests = [], resolutions = [];
25
+ const state = { key: "AKIAEXAMPLEKEY1234567", secret: "example-secret-not-live/+=", session: undefined,
26
+ status: 200, xml: listXml, response: undefined };
27
+ const options = {
28
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { aws: {
29
+ provider: provider.id, displayName: "My AWS", accountMode: "shared", scopes: provider.id === "aws-s3" ? ["read", "write"] : [],
30
+ authentication: { method: "api-key", secretRef: "env:AWS_SECRET_ACCESS_KEY" },
31
+ settings: { region: "us-east-1", accessKeyIdRef: "env:AWS_ACCESS_KEY_ID", ...(provider.id === "aws-s3" ? { bucket: "app-files" } : { workgroup: "primary" }) }
32
+ } }, extensions: { fromCli: true } },
33
+ providers: [provider], authorize: async (owner) => owner,
34
+ resolveReference: async (ref) => {
35
+ resolutions.push(ref);
36
+ return { "env:AWS_ACCESS_KEY_ID": state.key, "env:AWS_SECRET_ACCESS_KEY": state.secret, "env:AWS_SESSION_TOKEN": state.session }[ref];
37
+ },
38
+ store: createFileConnectionStore({ directory, protection }),
39
+ fetchImpl: async (address, init) => {
40
+ const headers = new Headers(init.headers);
41
+ const target = headers.get("x-amz-target")?.split(".").at(-1);
42
+ requests.push({ url: new URL(address), init, headers, target, body: init.body && JSON.parse(init.body) });
43
+ if (provider.id === "aws-s3") return new Response(state.xml, { status: state.status, headers: { "content-type": "application/xml" } });
44
+ const response = state.response ?? { GetWorkGroup: workgroup, StartQueryExecution: { QueryExecutionId: "query-1" }, GetQueryExecution: query, GetQueryResults: results, StopQueryExecution: {} }[target];
45
+ return Response.json(response, { status: state.status });
46
+ }
47
+ };
48
+ return { service: createConnectionService(options), options, requests, resolutions, state, directory, protection };
49
+ }
50
+
51
+ for (const provider of [awsS3Provider, awsAthenaProvider]) {
52
+ test(`${provider.name} signs SDK requests with explicit credentials and persists only protected connection state`, async (t) => {
53
+ const { service, options, requests, state, directory, protection } = await fixture(t, provider);
54
+ const connected = await service.connectApiKey(input);
55
+ assert.equal(connected.status, "connected");
56
+ const { headers, url, init } = requests[0];
57
+ const serviceName = provider.id === "aws-s3" ? "s3" : "athena";
58
+ assert.equal(url.origin, `https://${serviceName}.us-east-1.amazonaws.com`);
59
+ assert.match(headers.get("authorization"), new RegExp(`AWS4-HMAC-SHA256 Credential=${state.key}/\\d{8}/us-east-1/${serviceName}/aws4_request`));
60
+ assert.equal(init.redirect, "error");
61
+ assert.equal(init.credentials, "omit");
62
+ assert.equal(headers.has("cookie"), false);
63
+ assert.equal(url.href.includes(state.secret), false);
64
+ for (const name of await readdir(directory)) {
65
+ const file = await readFile(path.join(directory, name), "utf8");
66
+ assert.equal(file.includes(state.secret), false);
67
+ assert.equal(file.includes(state.key), false);
68
+ }
69
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
70
+ assert.deepEqual(await restarted.status(input), connected);
71
+ state.key = "AKIAROTATEDKEY1234567";
72
+ state.secret = "rotated-secret";
73
+ await restarted.invoke({ ...input, operation: provider.checkOperation });
74
+ assert.ok(requests.at(-1).headers.get("authorization").includes(state.key));
75
+ assert.notEqual(requests.at(-1).headers.get("authorization"), headers.get("authorization"));
76
+ await restarted.disconnect(input);
77
+ assert.equal((await service.status(input)).status, "disconnected");
78
+ });
79
+
80
+ test(`${provider.name} rejects unknown regions, raw keys and invalid settings before resolving credentials`, async (t) => {
81
+ const { options, resolutions } = await fixture(t, provider);
82
+ const parse = (config) => parseIntegrationConfiguration(JSON.stringify(config), { providers: [provider] });
83
+ assert.deepEqual(parse(options.configuration), options.configuration);
84
+ for (const change of [{ region: "https://other.example" }, { region: "cn-north-1" }, { accessKeyIdRef: "AKIARAWKEY123456789" }, { sessionTokenRef: "raw-token" }, { sessionTokenRef: "https://key.example" },
85
+ ...(provider.id === "aws-s3" ? [{ bucket: "Bad_Bucket" }, { bucket: "192.168.0.1" }, { bucket: "name--x-s3" }, { bucket: "a..b" }] : [{ workgroup: "invalid/group" }, { resultLocation: "https://bucket.example" }, { resultLocation: "s3://results/a?key=secret" }])]) {
86
+ const configuration = structuredClone(options.configuration);
87
+ Object.assign(configuration.integrations.aws.settings, change);
88
+ assert.throws(() => parse(configuration), { code: "integration_configuration_invalid" });
89
+ }
90
+ const defaults = structuredClone(options.configuration);
91
+ delete defaults.integrations.aws.settings.region;
92
+ if (provider.id === "aws-athena") delete defaults.integrations.aws.settings.workgroup;
93
+ assert.equal(parse(defaults).integrations.aws.settings.region, "us-east-1");
94
+ if (provider.id === "aws-athena") assert.equal(parse(defaults).integrations.aws.settings.workgroup, "primary");
95
+ assert.equal(resolutions.length, 0);
96
+ });
97
+
98
+ test(`${provider.name} fails closed on missing credentials and signs temporary credentials without a host fallback`, async (t) => {
99
+ const { service, options, state, requests } = await fixture(t, provider);
100
+ for (const key of [undefined, "", "AKIA\nHEADER123456789", "ASIAEXAMPLEKEY1234567"]) {
101
+ state.key = key;
102
+ await assert.rejects(service.connectApiKey(input), { code: "connector_binding_missing" });
103
+ }
104
+ assert.equal(requests.length, 0);
105
+ options.configuration.integrations.aws.settings.sessionTokenRef = "env:AWS_SESSION_TOKEN";
106
+ const temporary = createConnectionService(options);
107
+ await assert.rejects(temporary.connectApiKey(input), { code: "connector_binding_missing" });
108
+ state.session = "session/+=value";
109
+ await temporary.connectApiKey(input);
110
+ assert.equal(requests[0].headers.get("x-amz-security-token"), state.session);
111
+ assert.ok(requests[0].headers.get("authorization").includes("x-amz-security-token"));
112
+ state.session = "rotated/session";
113
+ await temporary.invoke({ ...input, operation: provider.checkOperation });
114
+ assert.equal(requests[1].headers.get("x-amz-security-token"), state.session);
115
+ });
116
+
117
+ test(`${provider.name} isolates ownership, binds settings and authorizes before resolving secrets`, async (t) => {
118
+ const { service, options, requests, resolutions } = await fixture(t, provider);
119
+ await service.connectApiKey(input);
120
+ for (const owner of [{ ...context, subjectId: "other" }, { ...context, applicationId: "other" }]) {
121
+ await assert.rejects(service.invoke({ ...input, context: owner, operation: provider.checkOperation }), { code: "connector_reconnect_required" });
122
+ }
123
+ const configuration = structuredClone(options.configuration);
124
+ configuration.integrations.aws.settings.region = "ap-southeast-2";
125
+ const changed = createConnectionService({ ...options, configuration });
126
+ assert.equal((await changed.status(input)).status, "reconnect-required");
127
+ await assert.rejects(changed.invoke({ ...input, operation: provider.checkOperation }), { code: "connector_reconnect_required" });
128
+ const count = resolutions.length;
129
+ const denied = createConnectionService({ ...options, authorize: async () => null });
130
+ await assert.rejects(denied.invoke({ ...input, operation: provider.checkOperation }), { code: "connector_access_denied" });
131
+ assert.equal(requests.length, 1);
132
+ assert.equal(resolutions.length, count);
133
+ });
134
+
135
+ test(`${provider.name} maps AWS failures without disclosing response bodies or retrying`, async (t) => {
136
+ const { service, state, requests } = await fixture(t, provider);
137
+ for (const [status, name, code] of [[403, "AccessDeniedException", "connector_permission_denied"], [400, "TooManyRequestsException", "connector_rate_limited"], [400, "ExpiredTokenException", "connector_reconnect_required"], [301, "PermanentRedirect", "connector_configuration_invalid"], [404, "NoSuchBucket", "connector_not_found"], [500, "InternalError", "connector_provider_failed"]]) {
138
+ state.status = status;
139
+ state.xml = `<Error><Code>${name}</Code><Message>${state.secret}</Message></Error>`;
140
+ state.response = { __type: name, Message: state.secret };
141
+ const count = requests.length;
142
+ await assert.rejects(service.connectApiKey(input), (error) => {
143
+ assert.equal(error.code, code);
144
+ assert.equal(error.stack.includes(state.secret), false);
145
+ return true;
146
+ });
147
+ assert.equal(requests.length, count + 1);
148
+ assert.equal((await service.status(input)).status, "disconnected");
149
+ }
150
+ });
151
+
152
+ test(`${provider.name} cancels or times out a signed request without replay`, async (t) => {
153
+ const { options } = await fixture(t, provider);
154
+ for (const [failure, code] of [["AbortError", "connector_cancelled"], ["TimeoutError", "connector_provider_timeout"]]) {
155
+ const controller = new AbortController();
156
+ let calls = 0;
157
+ const interrupted = createConnectionService({ ...options, fetchImpl: async () => {
158
+ calls++;
159
+ controller.abort(new DOMException("Interrupted", failure));
160
+ throw new Error("Wrapped failure");
161
+ } });
162
+ await assert.rejects(interrupted.connectApiKey({ ...input, signal: controller.signal }), { code });
163
+ assert.equal(calls, 1);
164
+ }
165
+ });
166
+ }
167
+
168
+ test("S3 lists bounded, encoded pages and rejects malformed HTTP-200 responses", async (t) => {
169
+ const { service, state, requests } = await fixture(t);
170
+ await service.connectApiKey(input);
171
+ assert.equal(requests[0].url.pathname, "/app-files/");
172
+ assert.equal(requests[0].url.searchParams.get("list-type"), "2");
173
+ state.xml = listXml.replace("<IsTruncated>false</IsTruncated>", "<IsTruncated>true</IsTruncated><NextContinuationToken>opaque+/=</NextContinuationToken>");
174
+ const result = await service.invoke({ ...input, operation: "objects.list", input: { prefix: " photos/ ", delimiter: "/", maxKeys: 10, continuationToken: "opaque+/=" } });
175
+ assert.equal(result.NextContinuationToken, "opaque+/=");
176
+ assert.equal(result.Contents, undefined);
177
+ assert.equal(requests[1].url.searchParams.get("prefix"), " photos/ ");
178
+ assert.equal(requests[1].url.searchParams.get("continuation-token"), "opaque+/=");
179
+ state.xml = listXml.replace("<KeyCount>0</KeyCount>", "<KeyCount>1</KeyCount><EncodingType>url</EncodingType><Contents><Key>photo%20%2B%25.jpg</Key><Size>123</Size></Contents>");
180
+ const encoded = await service.invoke({ ...input, operation: "objects.list" });
181
+ assert.equal(encoded.EncodingType, "url");
182
+ assert.equal(encoded.Contents[0].Key, "photo%20%2B%25.jpg");
183
+ for (const xml of ["<ListBucketResult/>", listXml.replace("app-files", "other-bucket"), listXml.replace("false", "true"), listXml.replace("<KeyCount>0</KeyCount>", "<Contents><Key>a</Key></Contents>")]) {
184
+ state.xml = xml;
185
+ await assert.rejects(service.invoke({ ...input, operation: "objects.list" }), { code: "connector_response_invalid" });
186
+ }
187
+ for (const fields of [{ maxKeys: 1001 }, { maxKeys: 1.5 }, { delimiter: "x" }, { Bucket: "other" }, { endpoint: "https://other.example" }]) {
188
+ await assert.rejects(service.invoke({ ...input, operation: "objects.list", input: fields }), { code: "connector_input_invalid" });
189
+ }
190
+ });
191
+
192
+ test("S3 presigns GET and PUT for exact keys with bounded expiry, read/write policy and no network use", async (t) => {
193
+ const { service, options, state, requests } = await fixture(t);
194
+ await service.connectApiKey(input);
195
+ const key = " space/é +%name.txt ";
196
+ for (const [operation, method] of [["objects.downloadUrl", "GET"], ["objects.uploadUrl", "PUT"]]) {
197
+ const result = await service.invoke({ ...input, operation, input: { key, expiresInSeconds: 90 } });
198
+ const url = new URL(result.url);
199
+ assert.equal(result.method, method);
200
+ assert.equal(result.expiresInSeconds, 90);
201
+ assert.equal(url.origin, "https://s3.us-east-1.amazonaws.com");
202
+ assert.equal(decodeURIComponent(url.pathname), `/app-files/${key}`);
203
+ assert.equal(url.searchParams.get("X-Amz-Expires"), "90");
204
+ assert.ok(url.searchParams.get("X-Amz-Credential").startsWith(state.key + "/"));
205
+ assert.match(url.searchParams.get("X-Amz-Signature"), /^[a-f0-9]{64}$/u);
206
+ assert.equal(url.href.includes(state.secret), false);
207
+ }
208
+ for (const fields of [{ key: "../file" }, { key: "a/./file" }, { key: "é".repeat(513) }, { key: "a", expiresInSeconds: 901 }, { key: "a", expiresInSeconds: 0 }, { key: "a", Bucket: "other" }]) {
209
+ await assert.rejects(service.invoke({ ...input, operation: "objects.uploadUrl", input: fields }), { code: "connector_input_invalid" });
210
+ }
211
+ const configuration = structuredClone(options.configuration);
212
+ configuration.integrations.aws.scopes = ["read"];
213
+ const reader = createConnectionService({ ...options, configuration });
214
+ await reader.connectApiKey(input);
215
+ await assert.rejects(reader.invoke({ ...input, operation: "objects.uploadUrl", input: { key: "file" } }), { code: "connector_scope_missing" });
216
+ assert.equal(requests.length, 2, "Only connection checks contact S3; URL construction is not upload/download proof.");
217
+ });
218
+
219
+ test("S3 application composition transfers exact binary bytes through signed GET and PUT", async (t) => {
220
+ const { service } = await fixture(t);
221
+ await service.connectApiKey(input);
222
+ const bytes = new Uint8Array([0, 255, 80, 65, 82, 49, 13, 10, 128]);
223
+ const signal = AbortSignal.timeout(5000);
224
+ const transfers = [];
225
+ const fetchImpl = async (address, init) => {
226
+ const url = new URL(address);
227
+ assert.equal(url.origin, "https://s3.us-east-1.amazonaws.com");
228
+ assert.equal(decodeURIComponent(url.pathname), "/app-files/data/é +%.parquet");
229
+ assert.match(url.searchParams.get("X-Amz-Signature"), /^[a-f0-9]{64}$/u);
230
+ assert.equal(init.credentials, "omit");
231
+ assert.equal(init.redirect, "error");
232
+ assert.equal(init.signal, signal);
233
+ assert.equal(new Headers(init.headers).has("authorization"), false);
234
+ transfers.push(init.method);
235
+ if (init.method === "PUT") {
236
+ assert.deepEqual(new Uint8Array(await new Response(init.body).arrayBuffer()), bytes);
237
+ return new Response(null, { status: 200, headers: { etag: '"object-version"' } });
238
+ }
239
+ return new Response(new ReadableStream({ start(controller) {
240
+ controller.enqueue(bytes.slice(0, 3)); controller.enqueue(bytes.slice(3)); controller.close();
241
+ } }));
242
+ };
243
+ const args = { service, ...input, key: "data/é +%.parquet", signal, fetchImpl };
244
+ const uploaded = await transferS3Object({ ...args, direction: "upload", body: bytes });
245
+ assert.equal(uploaded.headers.get("etag"), '"object-version"');
246
+ const downloaded = await transferS3Object({ ...args, direction: "download" });
247
+ assert.deepEqual(new Uint8Array(await downloaded.arrayBuffer()), bytes);
248
+ assert.deepEqual(transfers, ["PUT", "GET"]);
249
+ });
250
+
251
+ test("S3 transfer composition preserves denial, HTTP failures and uncertain writes without replay", async (t) => {
252
+ const { service, options } = await fixture(t);
253
+ await service.connectApiKey(input);
254
+ let calls = 0;
255
+ const args = { service, ...input, key: "data.bin", signal: AbortSignal.timeout(5000), direction: "upload", body: new Uint8Array([1]) };
256
+ await assert.rejects(transferS3Object({ ...args, fetchImpl: async () => {
257
+ calls++; throw new Error("network failure containing secret signed URL");
258
+ } }), (error) => {
259
+ assert.match(error.message, /completion is unknown/);
260
+ assert.equal(error.stack.includes("secret signed URL"), false);
261
+ assert.equal(error.cause, undefined);
262
+ return true;
263
+ });
264
+ assert.equal(calls, 1);
265
+ for (const status of [403, 404, 500]) {
266
+ await assert.rejects(transferS3Object({ ...args, fetchImpl: async () => {
267
+ calls++; return new Response("private provider error", { status });
268
+ } }), new RegExp(`HTTP ${status}`));
269
+ }
270
+ assert.equal(calls, 4);
271
+ const denied = createConnectionService({ ...options, authorize: async () => null });
272
+ await assert.rejects(transferS3Object({ ...args, service: denied, fetchImpl: async () => { calls++; } }), { code: "connector_access_denied" });
273
+ await assert.rejects(transferS3Object({ ...args, signal: AbortSignal.abort(), fetchImpl: async () => { calls++; } }), { name: "AbortError" });
274
+ assert.equal(calls, 4);
275
+ const partial = await transferS3Object({ ...args, direction: "download", fetchImpl: async () => new Response(
276
+ new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array([1])); controller.error(new Error("truncated body")); } })
277
+ ) });
278
+ await assert.rejects(partial.arrayBuffer(), /truncated body/);
279
+ });
280
+
281
+ test("S3 local permissions never masquerade as an IAM grant and cannot bypass connection verification", async (t) => {
282
+ const { options, requests, resolutions, state } = await fixture(t);
283
+ options.configuration.integrations.aws.scopes = ["write"];
284
+ assert.throws(() => createConnectionService(options), (error) => {
285
+ assert.equal(error.fieldErrors["integrations.aws.scopes"], "Include this provider's required permissions.");
286
+ return true;
287
+ });
288
+ assert.equal(resolutions.length, 0);
289
+ options.configuration.integrations.aws.scopes = ["read"];
290
+ const service = createConnectionService(options);
291
+ assert.deepEqual((await service.connectApiKey(input)).grantedScopes, []);
292
+ state.status = 403;
293
+ state.xml = "<Error><Code>ExpiredToken</Code><Message>Expired session</Message></Error>";
294
+ await assert.rejects(service.invoke({ ...input, operation: "objects.list" }), { code: "connector_reconnect_required" });
295
+ assert.equal((await service.status(input)).status, "reconnect-required");
296
+ assert.equal(requests.length, 2);
297
+ state.status = 200;
298
+ state.xml = listXml;
299
+ state.key = "AKIARENEWEDKEY1234567";
300
+ state.secret = "renewed-secret";
301
+ assert.equal((await service.connectApiKey(input)).status, "connected");
302
+ assert.match(requests.at(-1).headers.get("authorization"), /AKIARENEWEDKEY1234567/);
303
+ assert.deepEqual(await service.disconnect(input), { status: "disconnected" });
304
+ assert.equal((await service.status(input)).status, "disconnected");
305
+ await assert.rejects(service.invoke({ ...input, operation: "objects.downloadUrl", input: { key: "private.txt" } }), { code: "connector_reconnect_required" });
306
+ assert.equal(requests.length, 3, "Disconnect is local and cannot revoke an already issued presigned URL.");
307
+ });
308
+
309
+ test("Athena never runs SQL on connect and sends the caller's exact statement and idempotency token", async (t) => {
310
+ const { service, options, requests } = await fixture(t, awsAthenaProvider);
311
+ await service.connectApiKey(input);
312
+ assert.deepEqual(requests.map((r) => r.target), ["GetWorkGroup"]);
313
+ const configuration = structuredClone(options.configuration);
314
+ configuration.integrations.aws.settings.resultLocation = "s3://query-results/reports/";
315
+ let authorized;
316
+ const configured = createConnectionService({ ...options, configuration, authorize: async (owner, request) => { authorized = request; return owner; } });
317
+ await configured.connectApiKey(input);
318
+ const fields = { sql: " SELECT ' value ' ", clientRequestToken: "idempotency-key-for-this-query-0001", database: "reports", catalog: "AwsDataCatalog" };
319
+ const result = await configured.invoke({ ...input, operation: "query.start", input: fields });
320
+ assert.equal(result.QueryExecutionId, "query-1");
321
+ assert.deepEqual(authorized.input, fields);
322
+ assert.deepEqual(requests.at(-1).body, { QueryString: fields.sql, ClientRequestToken: fields.clientRequestToken, WorkGroup: "primary", QueryExecutionContext: { Database: "reports", Catalog: "AwsDataCatalog" }, ResultConfiguration: { OutputLocation: "s3://query-results/reports/" } });
323
+ await configured.invoke({ ...input, operation: "query.start", input: fields });
324
+ assert.equal(requests.at(-1).body.ClientRequestToken, fields.clientRequestToken);
325
+ for (const invalid of [{ sql: "SELECT 1" }, { ...fields, clientRequestToken: "short" }, { ...fields, sql: "" }, { ...fields, sql: " " }, { ...fields, sql: "é".repeat(131073) }, { ...fields, workgroup: "other" }, { ...fields, resultLocation: "s3://other/" }]) {
326
+ await assert.rejects(configured.invoke({ ...input, operation: "query.start", input: invalid }), { code: "connector_input_invalid" });
327
+ }
328
+ });
329
+
330
+ test("Athena verifies workgroup before status/results/cancel and preserves result headers, empty cells and cursors", async (t) => {
331
+ const { service, state, requests } = await fixture(t, awsAthenaProvider);
332
+ await service.connectApiKey(input);
333
+ assert.deepEqual(await service.invoke({ ...input, operation: "query.status", input: { queryId: "query-1" } }), query);
334
+ assert.deepEqual(await service.invoke({ ...input, operation: "query.results", input: { queryId: "query-1", maxResults: 25, nextToken: "opaque +/=" } }), results);
335
+ assert.deepEqual(requests.slice(-2).map((r) => r.target), ["GetQueryExecution", "GetQueryResults"]);
336
+ assert.deepEqual(requests.at(-1).body, { QueryExecutionId: "query-1", MaxResults: 25, QueryResultType: "DATA_ROWS", NextToken: "opaque +/=" });
337
+ assert.deepEqual(await service.invoke({ ...input, operation: "query.cancel", input: { queryId: "query-1" } }), {});
338
+ assert.deepEqual(requests.slice(-2).map((r) => r.target), ["GetQueryExecution", "StopQueryExecution"]);
339
+ state.response = { QueryExecution: { ...query.QueryExecution, WorkGroup: "private" } };
340
+ for (const operation of ["query.status", "query.results", "query.cancel"]) {
341
+ const count = requests.length;
342
+ await assert.rejects(service.invoke({ ...input, operation, input: { queryId: "query-1" } }), { code: "connector_access_denied" });
343
+ assert.equal(requests.length, count + 1);
344
+ assert.equal(requests.at(-1).target, "GetQueryExecution");
345
+ }
346
+ state.response = { QueryExecution: { ...query.QueryExecution, QueryExecutionId: "other-query" } };
347
+ await assert.rejects(service.invoke({ ...input, operation: "query.status", input: { queryId: "query-1" } }), { code: "connector_response_invalid" });
348
+ });
349
+
350
+ test("Athena rejects malformed verification and results, validates pagination and returns all job states", async (t) => {
351
+ const { service, state, options } = await fixture(t, awsAthenaProvider);
352
+ for (const response of [{}, { WorkGroup: { Name: "other", State: "ENABLED" } }, { WorkGroup: { Name: "primary" } }]) {
353
+ state.response = response;
354
+ await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
355
+ assert.equal((await service.status(input)).status, "disconnected");
356
+ }
357
+ state.response = undefined;
358
+ await service.connectApiKey(input);
359
+ for (const status of ["QUEUED", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED"]) {
360
+ state.response = { QueryExecution: { ...query.QueryExecution, Status: { State: status } } };
361
+ const value = await service.invoke({ ...input, operation: "query.status", input: { queryId: "query-1" } });
362
+ assert.equal(value.QueryExecution.Status.State, status);
363
+ }
364
+ for (const fields of [{ queryId: " " }, { queryId: "query-1", maxResults: 0 }, { queryId: "query-1", maxResults: 1.5 }, { queryId: "query-1", nextToken: "" }, { queryId: "query-1", queryResultType: "DATA_MANIFEST" }]) {
365
+ await assert.rejects(service.invoke({ ...input, operation: "query.results", input: fields }), { code: "connector_input_invalid" });
366
+ }
367
+ const malformed = createConnectionService({ ...options, fetchImpl: async (_, init) => {
368
+ const target = new Headers(init.headers).get("x-amz-target");
369
+ return Response.json(target.endsWith("GetQueryExecution") ? query : { ResultSet: {} });
370
+ } });
371
+ await assert.rejects(malformed.invoke({ ...input, operation: "query.results", input: { queryId: "query-1" } }), { code: "connector_response_invalid" });
372
+ });
373
+
374
+ test("Athena browses catalogs, databases and typed table metadata with opaque pagination", async (t) => {
375
+ const { service, options, state, requests, resolutions } = await fixture(t, awsAthenaProvider);
376
+ await service.connectApiKey(input);
377
+ const cases = [
378
+ ["catalogs.list", "ListDataCatalogs", {}, { DataCatalogsSummary: [{ CatalogName: "AwsDataCatalog", Type: "GLUE" }] }],
379
+ ["databases.list", "ListDatabases", { catalog: "AwsDataCatalog" }, { DatabaseList: [{ Name: "reports", Description: "Reporting data" }] }],
380
+ ["tables.list", "ListTableMetadata", { catalog: "AwsDataCatalog", database: "reports" }, { TableMetadataList: [{ Name: "orders", Columns: [{ Name: "amount", Type: "decimal(38,2)" }], PartitionKeys: [{ Name: "day", Type: "date" }] }] }]
381
+ ];
382
+ for (const [operation, target, fields, response] of cases) {
383
+ state.response = { ...response, NextToken: "opaque +/=" };
384
+ const value = await service.invoke({ ...input, operation, input: { ...fields, maxResults: 2, nextToken: "previous +/=" } });
385
+ assert.deepEqual(value, state.response);
386
+ assert.equal(requests.at(-1).target, target);
387
+ assert.deepEqual(requests.at(-1).body, { WorkGroup: "primary", MaxResults: 2, NextToken: "previous +/=",
388
+ ...(fields.catalog ? { CatalogName: fields.catalog } : {}), ...(fields.database ? { DatabaseName: fields.database } : {}) });
389
+ state.response = { [Object.keys(response)[0]]: [] };
390
+ assert.deepEqual(await service.invoke({ ...input, operation, input: fields }), state.response);
391
+ state.response = { ...response, NextToken: "" };
392
+ await assert.rejects(service.invoke({ ...input, operation, input: fields }), { code: "connector_response_invalid" });
393
+ const count = requests.length;
394
+ await assert.rejects(service.invoke({ ...input, operation, input: { ...fields, maxResults: 51 } }), { code: "connector_input_invalid" });
395
+ assert.equal(requests.length, count);
396
+ }
397
+ state.response = { TableMetadataList: [{ Name: "orders", Columns: [{ Name: "bad", Type: 42 }] }] };
398
+ await assert.rejects(service.invoke({ ...input, operation: "tables.list", input: { catalog: "AwsDataCatalog", database: "reports" } }), { code: "connector_response_invalid" });
399
+ await assert.rejects(service.invoke({ ...input, operation: "catalogs.list", input: { maxResults: 1 } }), { code: "connector_input_invalid" });
400
+ const denied = createConnectionService({ ...options, authorize: async () => { throw new Error("Catalog not authorized"); } });
401
+ const count = resolutions.length;
402
+ await assert.rejects(denied.invoke({ ...input, operation: "databases.list", input: { catalog: "private" } }), /Catalog not authorized/);
403
+ assert.equal(resolutions.length, count);
404
+ assert.equal(requests.some((r) => r.target === "StartQueryExecution"), false);
405
+ });
@@ -0,0 +1,71 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
7
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
8
+ import { brevoProvider } from "../src/server/brevo.js";
9
+
10
+ test("Brevo explicitly sends messages and campaigns, manages contacts and retains DNS and delivery state", async t => {
11
+ const directory = await mkdtemp(path.join(tmpdir(), "brevo-repair-")); t.after(() => rm(directory, { recursive: true, force: true }));
12
+ const requests = []; const state = { response: { contacts: [], count: 0 }, status: 200, deny: false, fail: false };
13
+ const identity = { context: { applicationId: "booking-app", subjectId: "owner" }, integrationId: "messaging" };
14
+ const service = createConnectionService({ configuration: { schemaVersion: 1, registrations: {}, integrations: {
15
+ messaging: { provider: "brevo", accountMode: "shared", scopes: [], authentication: { method: "api-key", secretRef: "env:BREVO_API_KEY" } }
16
+ } }, providers: [brevoProvider], authorize: async context => { if (state.deny) throw new Error("denied"); return context; },
17
+ resolveReference: async () => "brevo-fixture-key",
18
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(3) }, activeKeyId: "current" }) }),
19
+ fetchImpl: async (url, init) => { requests.push({ url: new URL(url), init }); if (state.fail) throw new Error("uncertain send");
20
+ return state.response === null ? new Response(null, { status: 204 }) : Response.json(state.response, { status: state.status }); }
21
+ });
22
+ const invoke = (operation, input = {}) => service.invoke({ ...identity, operation, input }); await service.connectApiKey(identity);
23
+ const sender = { email: "bookings@example.test", name: "Dog and Groom" };
24
+ const email = { sender, to: [{ email: "customer@example.test" }], subject: "Booking confirmed", textContent: "Tuesday at 10" };
25
+ const dns = { brevo_code: { host_name: "example.test", type: "TXT", value: "brevo-code:fixture", status: false },
26
+ dkim_record: { host_name: "mail._domainkey.example.test", type: "TXT", value: "fixture-dkim", status: false } };
27
+ for (const [operation, method, endpoint, input, response] of [
28
+ ["email.send", "POST", "smtp/email", email, { messageId: "<accepted-1>" }],
29
+ ["email.send", "POST", "smtp/email", { sender, to: email.to, templateId: 2, params: { APPOINTMENT: "Tuesday" } }, { messageId: "<accepted-2>" }],
30
+ ["sms.send", "POST", "transactionalSMS/sms", { sender: "DogGroom", recipient: "+61400000000", type: "transactional", content: "Your appointment is tomorrow" }, { messageId: 12, reference: "accepted", usedCredits: 1, remainingCredits: 9 }],
31
+ ["contacts.create", "POST", "contacts", { email: "customer@example.test", attributes: { FIRSTNAME: "Sam" }, listIds: [3], updateEnabled: false }, { id: 42 }],
32
+ ["contacts.update", "PUT", "contacts/customer%40example.test", { resource: "customer@example.test", emailBlacklisted: true }, null],
33
+ ["contacts.get", "GET", "contacts/customer%40example.test", { resource: "customer@example.test" }, { id: 42, emailBlacklisted: true }],
34
+ ["lists.create", "POST", "contacts/lists", { name: "Consenting customers", folderId: 1 }, { id: 3 }],
35
+ ["lists.addContacts", "POST", "contacts/lists/3/contacts/add", { resource: 3, emails: ["customer@example.test"] }, { contacts: { success: ["customer@example.test"], failure: ["missing@example.test"] } }],
36
+ ["lists.removeContacts", "POST", "contacts/lists/3/contacts/remove", { resource: 3, emails: ["customer@example.test"] }, { contacts: { success: ["customer@example.test"], failure: [] } }],
37
+ ["senders.create", "POST", "senders", sender, { id: 2, dkimError: true, spfError: false }],
38
+ ["domains.create", "POST", "senders/domains", { name: "example.test" }, { id: 4, domain_name: "example.test", message: "Created", dns_records: dns }],
39
+ ["domains.get", "GET", "senders/domains/example.test", { resource: "example.test" }, { domain: "example.test", verified: false, authenticated: false, dns_records: dns }],
40
+ ["domains.authenticate", "PUT", "senders/domains/example.test/authenticate", { resource: "example.test" }, { domain_name: "example.test", message: "Check requested" }],
41
+ ["campaigns.create", "POST", "emailCampaigns", { name: "Spring offers", sender, subject: "Spring", htmlContent: "<p>Offer</p>", recipients: { listIds: [3], exclusionListIds: [4] } }, { id: 7 }],
42
+ ["campaigns.get", "GET", "emailCampaigns/7", { resource: 7 }, { id: 7, status: "draft", statistics: {} }],
43
+ ["campaigns.sendTest", "POST", "emailCampaigns/7/sendTest", { resource: 7, emailTo: ["owner@example.test"] }, null],
44
+ ["campaigns.sendNow", "POST", "emailCampaigns/7/sendNow", { resource: 7 }, null],
45
+ ["events.create", "POST", "events", { event_name: "booking_completed", identifiers: { email_id: "customer@example.test" }, event_properties: { bookingId: "b-1" } }, null]
46
+ ]) { state.response = response; assert.deepEqual(await invoke(operation, input).catch(error => { error.message = `${operation}: ${error.message}`; throw error; }), response);
47
+ const request = requests.at(-1); assert.equal(request.url.pathname, `/v3/${endpoint}`); assert.equal(request.init.method, method);
48
+ if (method !== "GET") { const { resource, ...body } = input;
49
+ if (Object.keys(body).length) assert.deepEqual(JSON.parse(request.init.body), body); else assert.equal(request.init.body, undefined); }
50
+ assert.equal(new Headers(request.init.headers).get("api-key"), "brevo-fixture-key"); assert.equal(request.init.redirect, "error");
51
+ }
52
+ for (const [operation, key, endpoint] of [["folders.list", "folders", "contacts/folders"], ["lists.list", "lists", "contacts/lists"],
53
+ ["senders.list", "senders", "senders"], ["campaigns.list", "campaigns", "emailCampaigns"], ["email.events", "events", "smtp/statistics/events"],
54
+ ["sms.events", "events", "transactionalSMS/statistics/events"]]) {
55
+ state.response = { [key]: operation.includes("events") ? [{ event: "hardBounces", messageId: "<accepted-1>", reason: "No mailbox" }] : [] };
56
+ assert.deepEqual(await invoke(operation), state.response); assert.equal(requests.at(-1).url.pathname, `/v3/${endpoint}`);
57
+ }
58
+ const before = requests.length;
59
+ for (const [operation, input] of [["email.send", { ...email, to: [] }], ["email.send", { sender, to: email.to }],
60
+ ["sms.send", { sender: "Too-long-name", recipient: "bad", type: "transactional", content: "Hi" }],
61
+ ["sms.send", { sender: "Test", recipient: "61400000000", type: "transactional", content: "Hi", templateId: 2 }],
62
+ ["contacts.update", { resource: "customer@example.test" }], ["domains.get", { resource: "https://evil.test/" }],
63
+ ["campaigns.create", { name: "Missing audience", sender, subject: "Test", htmlContent: "Hi", recipients: { listIds: [] } }],
64
+ ["events.create", { event_name: "bad name", identifiers: {} }]
65
+ ]) await assert.rejects(invoke(operation, input));
66
+ state.deny = true; await assert.rejects(invoke("campaigns.sendNow", { resource: 7 })); state.deny = false; assert.equal(requests.length, before);
67
+ state.response = { message: "accepted without ID" }; await assert.rejects(invoke("email.send", email), { code: "connector_response_invalid" });
68
+ for (const status of [400, 403, 429]) { state.status = status; await assert.rejects(invoke("email.send", email)); }
69
+ state.status = 200; state.fail = true; const uncertain = requests.length; await assert.rejects(invoke("email.send", email));
70
+ assert.equal(requests.length, uncertain + 1);
71
+ });