@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,247 @@
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 { 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 { parseIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
9
+ import { shopifyProvider } from "../src/server/shopify.js";
10
+
11
+ const context = { applicationId: "app-one", subjectId: "store-one" };
12
+ const args = { context, integrationId: "shopify" };
13
+ const product = { id: "gid://shopify/Product/123", title: "Fixture product", handle: "fixture-product", status: "DRAFT" };
14
+
15
+ async function fixture(t, { method = "oauth2", scopes = ["read_products", "write_products"], assistantPolicy } = {}) {
16
+ const directory = await mkdtemp(path.join(tmpdir(), "shopify-connector-"));
17
+ t.after(() => rm(directory, { recursive: true, force: true }));
18
+ const configuration = { schemaVersion: 1, registrations: method === "oauth2" ? {
19
+ own: { source: "own", grantType: "client_credentials", clientId: "fixture-client", clientSecretRef: "env:SHOPIFY_SECRET" }
20
+ } : {}, integrations: { shopify: { provider: "shopify", accountMode: "shared", scopes,
21
+ authentication: method === "oauth2" ? { method, registrationRef: "own" } : { method, secretRef: "env:SHOPIFY_TOKEN" },
22
+ settings: { shopDomain: "fixture-store.myshopify.com" }, ...(assistantPolicy ? { assistantPolicy } : {}) } } };
23
+ const state = { time: Date.now(), tokenCount: 0, tokenPatch: {}, tokenStatus: 200, value: undefined,
24
+ status: 200, approved: false, denied: false, token: "private-api-token", hang: false };
25
+ const requests = []; const decisions = [];
26
+ const options = { configuration, providers: [{ ...shopifyProvider, requestTimeoutMs: 70 }], now: () => state.time,
27
+ authorize: async (owner, request) => { decisions.push(request); return state.denied ? null : { ...owner, approved: state.approved }; },
28
+ resolveReference: async (ref) => { assert.ok(["env:SHOPIFY_SECRET", "env:SHOPIFY_TOKEN"].includes(ref)); return ref === "env:SHOPIFY_SECRET" ? "private-client-secret" : state.token; },
29
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(11) }, activeKeyId: "current" }) }),
30
+ fetchImpl: async (address, init) => {
31
+ const url = new URL(String(address)); const headers = new Headers(init.headers); requests.push({ url, init });
32
+ assert.equal(url.origin, "https://fixture-store.myshopify.com");
33
+ assert.equal(init.method, "POST");
34
+ if (url.pathname === "/admin/oauth/access_token") {
35
+ const body = new URLSearchParams(init.body);
36
+ assert.equal(body.get("grant_type"), "client_credentials"); assert.equal(body.get("client_id"), "fixture-client");
37
+ assert.equal(body.get("client_secret"), "private-client-secret"); assert.equal(body.has("redirect_uri"), false);
38
+ state.tokenCount++;
39
+ return Response.json(state.tokenStatus === 200 ? { access_token: `private-token-${state.tokenCount}`, scope: scopes.join(","), expires_in: 86399,
40
+ ...state.tokenPatch } : { error: "invalid_client", error_description: "private-provider-detail" }, { status: state.tokenStatus });
41
+ }
42
+ assert.equal(url.pathname, "/admin/api/2026-07/graphql.json"); assert.equal(url.search, "");
43
+ assert.equal(init.redirect, "error"); assert.equal(init.credentials, "omit"); assert.equal(headers.has("authorization"), false);
44
+ assert.equal(headers.get("x-shopify-access-token"), method === "oauth2" ? `private-token-${state.tokenCount}` : state.token);
45
+ if (state.hang) return new Promise((_, reject) => {
46
+ const timer = setTimeout(() => reject(new Error("Missing abort")), 1000);
47
+ const abort = () => { clearTimeout(timer); reject(init.signal.reason); };
48
+ if (init.signal.aborted) abort(); else init.signal.addEventListener("abort", abort, { once: true });
49
+ });
50
+ const body = JSON.parse(init.body);
51
+ let value = { data: { products: { nodes: [product], pageInfo: { hasNextPage: false, endCursor: "cursor-one" } } } };
52
+ for (const name of ["Create", "Update", "Delete"]) if (body.query.includes(`mutation ConnectorProduct${name}`)) {
53
+ value = { data: { [`product${name}`]: { ...(name === "Delete" ? { deletedProductId: product.id } : { product }), userErrors: [] } } };
54
+ }
55
+ if (typeof state.value === "string") return new Response(state.value, { status: state.status });
56
+ return Response.json(state.value === undefined ? value : state.value, { status: state.status });
57
+ }
58
+ };
59
+ const service = createConnectionService(options);
60
+ const connect = () => method === "oauth2" ? service.connectClientCredentials(args) : service.connectApiKey(args);
61
+ return { directory, options, state, requests, decisions, service, connect };
62
+ }
63
+
64
+ test("Shopify verifies and persists organization-app grants with the Shopify header, renewal and bounded product pages", async (t) => {
65
+ const f = await fixture(t); assert.equal((await f.connect()).status, "connected");
66
+ const restarted = createConnectionService(f.options); assert.equal((await restarted.status(args)).status, "connected");
67
+ const result = await restarted.invoke({ ...args, operation: "products.list", input: { first: 1, after: "cursor & next", query: "status:active" } });
68
+ assert.deepEqual(result.data.products.nodes, [product]);
69
+ assert.deepEqual(JSON.parse(f.requests.at(-1).init.body).variables, { first: 1, after: "cursor & next", query: "status:active" });
70
+ f.state.time += 86400_000; await restarted.invoke({ ...args, operation: "products.list" }); assert.equal(f.state.tokenCount, 2);
71
+ for (const file of await readdir(f.directory)) {
72
+ const value = await readFile(path.join(f.directory, file), "utf8");
73
+ for (const secret of ["private-client-secret", "private-token-1", "private-token-2"]) assert.equal(value.includes(secret), false);
74
+ }
75
+ f.state.value = { data: { products: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } } } };
76
+ assert.deepEqual(await restarted.invoke({ ...args, operation: "products.list" }), f.state.value);
77
+ });
78
+
79
+ test("Shopify existing Admin tokens rotate through the reference and never invoke an OAuth endpoint", async (t) => {
80
+ const f = await fixture(t, { method: "api-key" }); await f.connect(); f.state.token = "private-rotated-token";
81
+ await f.service.invoke({ ...args, operation: "products.list" }); assert.equal(f.state.tokenCount, 0);
82
+ assert.equal((await f.service.status(args)).status, "connected");
83
+ await f.service.disconnect(args); assert.equal((await f.service.status(args)).status, "disconnected");
84
+ });
85
+
86
+ test("Shopify sends bounded product mutations with variables, defaults new products to draft and exposes confirmed deletion", async (t) => {
87
+ const f = await fixture(t); await f.connect();
88
+ const title = 'A "quoted" product';
89
+ await f.service.invoke({ ...args, operation: "products.create", input: { product: { title, tags: ["One", "Two"] } } });
90
+ const created = JSON.parse(f.requests.at(-1).init.body);
91
+ assert.deepEqual(created.variables, { product: { title, tags: ["One", "Two"], status: "DRAFT" } }); assert.equal(created.query.includes(title), false);
92
+ await f.service.invoke({ ...args, operation: "products.update", input: { product: { id: product.id, title: "New title", descriptionHtml: "", tags: [], status: "ARCHIVED" } } });
93
+ assert.deepEqual(JSON.parse(f.requests.at(-1).init.body).variables.product, { id: product.id, title: "New title", descriptionHtml: "", tags: [], status: "ARCHIVED" });
94
+ const deleted = await f.service.invoke({ ...args, operation: "products.delete", input: { input: { id: product.id } } });
95
+ assert.equal(deleted.data.productDelete.deletedProductId, product.id);
96
+ assert.match(JSON.parse(f.requests.at(-1).init.body).query, /synchronous: true/u);
97
+ });
98
+
99
+ test("Shopify validates paging, product types, unknown fields and mutation sizes before transport", async (t) => {
100
+ const f = await fixture(t); await f.connect(); const count = f.requests.length;
101
+ for (const input of [{ first: 0 }, { first: 101 }, { first: 1.5 }, { after: "" }, { query: "x".repeat(1025) }, { shopDomain: "other.myshopify.com" }, { queryText: "mutation {}" }]) {
102
+ await assert.rejects(f.service.invoke({ ...args, operation: "products.list", input }));
103
+ }
104
+ for (const input of [{}, { product: {} }, { product: { title: "" } }, { product: { title: "x", tags: Array(251).fill("x") } },
105
+ { product: { title: "x", price: "5.00" } }, { product: { title: "x", status: "PUBLISHED" } }]) {
106
+ await assert.rejects(f.service.invoke({ ...args, operation: "products.create", input }));
107
+ }
108
+ for (const input of [{ product: { id: product.id } }, { product: { id: "gid://shopify/Order/123", title: "New" } }]) {
109
+ await assert.rejects(f.service.invoke({ ...args, operation: "products.update", input }));
110
+ }
111
+ await assert.rejects(f.service.invoke({ ...args, operation: "products.delete", input: { id: product.id } }));
112
+ assert.equal(f.requests.length, count);
113
+ });
114
+
115
+ test("Shopify preserves read-only configuration and actual OAuth grants even when assistant approval is granted", async (t) => {
116
+ for (const method of ["oauth2", "api-key"]) {
117
+ const f = await fixture(t, { method, scopes: ["read_products"] }); await f.connect(); const count = f.requests.length;
118
+ f.state.approved = true;
119
+ const assistant = createConnectionService({ ...f.options, executionMode: "assistant" });
120
+ await assert.rejects(assistant.invoke({ ...args, operation: "products.create", input: { product: { title: "New" } } }), { code: "connector_scope_missing" });
121
+ assert.equal(f.requests.length, count);
122
+ }
123
+ const f = await fixture(t); f.state.tokenPatch.scope = "read_products"; await f.connect();
124
+ await assert.rejects(f.service.invoke({ ...args, operation: "products.create", input: { product: { title: "New" } } }), { code: "connector_scope_missing" });
125
+ const write = await fixture(t, { scopes: ["write_products"] }); await write.connect(); await write.service.invoke({ ...args, operation: "products.list" });
126
+ });
127
+
128
+ test("Shopify packaged guide uses the actual portable schema and all captured assistant action names", async () => {
129
+ const guide = await readFile(new URL("../docs/shopify.md", import.meta.url), "utf8");
130
+ const config = JSON.parse([...guide.matchAll(/```json\n([\s\S]*?)\n```/gu)][0][1]);
131
+ assert.deepEqual(parseIntegrationConfiguration(JSON.stringify(config), { providers: [shopifyProvider] }), config);
132
+ assert.equal(shopifyProvider.assistantActions.length, 16);
133
+ const labels = shopifyProvider.assistantActions.map((action) => action.label);
134
+ for (const label of ["Enable Shopify", "Connect your Shopify store", "Claim your store", "Create product", "Update product", "Delete product",
135
+ "Add product variant", "Update product variant", "Delete product variant", "Create discount code", "Update discount code", "Delete discount code",
136
+ "Create price rule", "Update price rule", "Delete price rule"]) assert.ok(labels.includes(label));
137
+ });
138
+
139
+ test("Shopify binds connection access to the application, subject, permanent shop and credentials", async (t) => {
140
+ const f = await fixture(t); await f.connect(); const count = f.requests.length;
141
+ for (const owner of [{ ...context, applicationId: "other" }, { ...context, subjectId: "other" }]) {
142
+ await assert.rejects(f.service.invoke({ ...args, context: owner, operation: "products.list" }), { code: "connector_reconnect_required" });
143
+ }
144
+ const configuration = structuredClone(f.options.configuration); configuration.integrations.shopify.settings.shopDomain = "other.myshopify.com";
145
+ const changed = createConnectionService({ ...f.options, configuration });
146
+ assert.equal((await changed.status(args)).status, "reconnect-required");
147
+ await assert.rejects(changed.invoke({ ...args, operation: "products.list" }), { code: "connector_reconnect_required" });
148
+ f.state.denied = true; await assert.rejects(f.service.invoke({ ...args, operation: "products.list" }), { code: "connector_access_denied" });
149
+ assert.equal(f.requests.length, count);
150
+ });
151
+
152
+ test("Shopify handles revoked, malformed and rejected grants without reporting connected", async (t) => {
153
+ for (const patch of [{ access_token: "" }, { expires_in: 0 }, { expires_in: "86399" }, { scope: "read_products write_products" }, { scope: "" }]) {
154
+ const f = await fixture(t); f.state.tokenPatch = patch;
155
+ await assert.rejects(f.connect(), { code: "connector_response_invalid" }); assert.equal((await f.service.status(args)).status, "disconnected");
156
+ }
157
+ const f = await fixture(t); await f.connect(); f.state.time += 86400_000; f.state.tokenStatus = 401;
158
+ await assert.rejects(f.service.invoke({ ...args, operation: "products.list" }), { code: "connector_reconnect_required" });
159
+ assert.equal((await f.service.status(args)).status, "reconnect-required");
160
+ });
161
+
162
+ test("Shopify rejects malformed GraphQL results and distinguishes throttling, denied access and user errors", async (t) => {
163
+ const f = await fixture(t); await f.connect();
164
+ for (const value of [null, {}, { data: null }, { data: { products: { nodes: [{}], pageInfo: { hasNextPage: false, endCursor: null } } } },
165
+ { data: { products: { nodes: [], pageInfo: { hasNextPage: true, endCursor: null } } } }, { errors: [{ message: "private-provider-detail" }], data: {} }, "<html>private</html>"]) {
166
+ f.state.value = value; await assert.rejects(f.service.invoke({ ...args, operation: "products.list" }), { code: "connector_response_invalid" });
167
+ }
168
+ for (const [code, expected] of [["THROTTLED", "connector_rate_limited"], ["ACCESS_DENIED", "connector_permission_denied"]]) {
169
+ f.state.value = { errors: [{ message: "private-provider-detail", extensions: { code } }] };
170
+ await assert.rejects(f.service.invoke({ ...args, operation: "products.list" }), (error) => error.code === expected && !error.message.includes("private"));
171
+ }
172
+ for (const name of ["Create", "Update", "Delete"]) {
173
+ f.state.value = { data: { [`product${name}`]: { userErrors: [{ field: ["title"], message: "private-provider-detail" }] } } };
174
+ const input = name === "Delete" ? { input: { id: product.id } } : { product: { ...(name === "Update" ? { id: product.id } : {}), title: "New" } };
175
+ const before = f.requests.length;
176
+ await assert.rejects(f.service.invoke({ ...args, operation: `products.${name.toLowerCase()}`, input }), { code: "connector_operation_rejected" });
177
+ assert.equal(f.requests.length, before + 1);
178
+ }
179
+ });
180
+
181
+ test("Shopify assistant permissions require host approval for the exact action and immutable input", async (t) => {
182
+ const f = await fixture(t); await f.connect();
183
+ const assistant = createConnectionService({ ...f.options, executionMode: "assistant" });
184
+ const input = { product: { title: "Approved product" } }; const call = { ...args, operation: "products.create", input };
185
+ const count = f.requests.length;
186
+ await assert.rejects(assistant.invoke(call), { code: "connector_approval_required" }); assert.equal(f.requests.length, count);
187
+ assert.deepEqual(f.decisions.at(-1).assistantPermission, { action: "products.create", decision: "ask" });
188
+ f.state.approved = true;
189
+ const changing = createConnectionService({ ...f.options, executionMode: "assistant", authorize: async (owner, request) => {
190
+ assert.deepEqual(request.input, input); input.product.title = "Caller changed"; request.input.product.title = "Policy changed";
191
+ request.assistantPermission.decision = "always"; return { ...owner, approved: true };
192
+ } });
193
+ await changing.invoke(call); assert.equal(JSON.parse(f.requests.at(-1).init.body).variables.product.title, "Approved product");
194
+ for (const approved of [false, undefined, "true", 1]) {
195
+ f.state.approved = approved; await assert.rejects(assistant.invoke(call), { code: "connector_approval_required" });
196
+ }
197
+ });
198
+
199
+ test("Shopify assistant never/always policies, overrides and disabled access preserve normal app authorization and cleanup", async (t) => {
200
+ const f = await fixture(t, { assistantPolicy: { enabled: true, defaultPermission: "never", actions: { "products.list": "always", "products.create": "ask" } } });
201
+ await f.connect(); const assistant = createConnectionService({ ...f.options, executionMode: "assistant" });
202
+ await assistant.invoke({ ...args, operation: "products.list" });
203
+ await assert.rejects(assistant.invoke({ ...args, operation: "products.delete", input: { input: { id: product.id } } }), { code: "connector_access_denied" });
204
+ f.state.denied = true; await assert.rejects(assistant.invoke({ ...args, operation: "products.list" }), { code: "connector_access_denied" }); f.state.denied = false;
205
+ const configuration = structuredClone(f.options.configuration); configuration.integrations.shopify.assistantPolicy.enabled = false;
206
+ const disabled = createConnectionService({ ...f.options, configuration, executionMode: "assistant" });
207
+ await assert.rejects(disabled.invoke({ ...args, operation: "products.list" }), { code: "connector_access_denied" });
208
+ assert.equal((await disabled.status(args)).status, "connected"); await disabled.disconnect(args);
209
+ assert.equal((await disabled.status(args)).status, "disconnected");
210
+ });
211
+
212
+ test("Shopify lifecycle permissions authorize declared host actions without inventing store activation or claiming", async (t) => {
213
+ const f = await fixture(t); const assistant = createConnectionService({ ...f.options, executionMode: "assistant" });
214
+ await assert.rejects(assistant.authorizeAssistantAction({ ...args, action: "claim", input: { store: "fixture-store" } }), { code: "connector_approval_required" });
215
+ f.state.approved = true; await assistant.authorizeAssistantAction({ ...args, action: "claim", input: { store: "fixture-store" } });
216
+ assert.deepEqual(f.decisions.at(-1).input, { store: "fixture-store" }); assert.equal(f.requests.length, 0);
217
+ await assert.rejects(assistant.authorizeAssistantAction({ ...args, action: "invented" }), { code: "connector_operation_unknown" });
218
+ await assert.rejects(f.service.authorizeAssistantAction({ ...args, action: "claim" }), { code: "connector_operation_unknown" });
219
+ assert.throws(() => createConnectionService({ ...f.options, executionMode: "browser-choice" }));
220
+ });
221
+
222
+ test("Shopify reports provider failures and cancellation without retrying a product mutation", async (t) => {
223
+ const f = await fixture(t); await f.connect();
224
+ for (const [status, expected] of [[401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
225
+ f.state.status = 200; await f.connect(); f.state.status = status; f.state.value = { error: "private-provider-detail" };
226
+ const count = f.requests.length;
227
+ await assert.rejects(f.service.invoke({ ...args, operation: "products.create", input: { product: { title: "New" } } }), { code: expected });
228
+ assert.equal(f.requests.length, count + 1); f.state.value = undefined;
229
+ }
230
+ f.state.status = 200; await f.connect(); f.state.hang = true;
231
+ const controller = new AbortController(); const pending = f.service.invoke({ ...args, operation: "products.list", signal: controller.signal });
232
+ setTimeout(() => controller.abort(), 10); await assert.rejects(pending, { code: "connector_cancelled" });
233
+ await assert.rejects(f.service.invoke({ ...args, operation: "products.list" }), { code: "connector_provider_timeout" });
234
+ });
235
+
236
+ test("Shopify portable configuration rejects foreign hosts, raw credentials and invalid assistant policy values", async (t) => {
237
+ const f = await fixture(t); const parse = (config) => parseIntegrationConfiguration(JSON.stringify(config), { providers: [shopifyProvider] });
238
+ assert.deepEqual(parse(f.options.configuration), f.options.configuration);
239
+ for (const shopDomain of ["", "https://fixture-store.myshopify.com", "fixture-store.myshopify.com:443", "fixture-store.myshopify.com/path", "whatever.com", "fixture-store.myshopify.com.attacker.test", "-store.myshopify.com", "store..myshopify.com"]) {
240
+ const config = structuredClone(f.options.configuration); config.integrations.shopify.settings.shopDomain = shopDomain;
241
+ assert.throws(() => parse(config), (error) => Boolean(error.fieldErrors["integrations.shopify.settings.shopDomain"]));
242
+ }
243
+ for (const assistantPolicy of [{ enabled: "false" }, { enabled: "perhaps" }, { defaultPermission: "allow" }, { actions: { invented: "always" } }, { actions: { claim: true } }]) {
244
+ const config = structuredClone(f.options.configuration); config.integrations.shopify.assistantPolicy = assistantPolicy; assert.throws(() => parse(config));
245
+ }
246
+ const config = structuredClone(f.options.configuration); config.registrations.own.clientSecretRef = "private-secret"; assert.throws(() => parse(config));
247
+ });
@@ -0,0 +1,320 @@
1
+ import { createHmac } from "node:crypto";
2
+ import assert from "node:assert/strict";
3
+ import test from "node:test";
4
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
8
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
9
+ import { parseIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
10
+ import { slackProvider, verifySlackRequest } from "../src/server/slack.js";
11
+
12
+ const callback = "https://app.example.test/slack/callback";
13
+ const context = { applicationId: "app-one", subjectId: "user-one" };
14
+ const input = { context, integrationId: "slack" };
15
+ const scopes = ["channels:read", "groups:read", "im:read", "mpim:read"];
16
+ const page = { ok: true, channels: [{ id: "C123", name: "general" }], response_metadata: { next_cursor: "next +&/=" } };
17
+
18
+ async function fixture(t, actor = "user", granted = scopes) {
19
+ const directory = await mkdtemp(path.join(tmpdir(), "slack-connector-"));
20
+ t.after(() => rm(directory, { recursive: true, force: true }));
21
+ const requests = [];
22
+ const state = { time: Date.now(), tokenCount: 0, tokenScope: granted.join(","), tokenStatus: 200, apiStatus: 200, page, rotation: true };
23
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(8) }, activeKeyId: "current" });
24
+ const token = (type) => ({ token_type: type, scope: state.tokenScope, access_token: `secret-${type}-access-${state.tokenCount}`,
25
+ ...(state.rotation ? { refresh_token: `secret-${type}-refresh-${state.tokenCount}+&`, expires_in: 60 } : {}) });
26
+ const options = {
27
+ configuration: { schemaVersion: 1, integrations: { slack: { provider: "slack", accountMode: "per-user", settings: { actor }, scopes: granted,
28
+ authentication: { method: "oauth2", registrationRef: "slack" } } }, registrations: {
29
+ slack: { source: "own", clientId: "111.222", clientSecretRef: "env:SLACK_SECRET", callbackUrlRef: "env:SLACK_CALLBACK" }
30
+ } }, providers: [slackProvider], authorize: async (owner) => owner, now: () => state.time,
31
+ resolveReference: async (ref) => ref === "env:SLACK_CALLBACK" ? callback : "secret-client",
32
+ store: createFileConnectionStore({ directory, protection }),
33
+ fetchImpl: async (address, init) => {
34
+ const url = new URL(String(address));
35
+ requests.push({ url, init });
36
+ assert.equal(url.origin, "https://slack.com");
37
+ if (state.interruptPath === url.pathname) {
38
+ state.controller.abort(new DOMException("secret-client", state.interruptName || "AbortError"));
39
+ throw init.signal.reason;
40
+ }
41
+ if (url.pathname === "/api/oauth.v2.access") {
42
+ assert.equal(init.method, "POST");
43
+ assert.equal(init.redirect, "manual");
44
+ const body = new URLSearchParams(init.body);
45
+ assert.equal(body.get("client_id"), "111.222");
46
+ assert.equal(body.get("client_secret"), "secret-client");
47
+ if (body.get("grant_type") === "refresh_token") assert.equal(body.get("refresh_token"), `secret-${actor}-refresh-${state.tokenCount}+&`);
48
+ state.tokenCount++;
49
+ if (state.tokenText !== undefined) return new Response(state.tokenText, { status: state.tokenStatus });
50
+ const reply = body.get("grant_type") === "refresh_token" ? { ok: true, ...token(actor) }
51
+ : { ok: true, ...token("bot"), authed_user: { id: "U123", ...token("user") } };
52
+ return Response.json(state.tokenResponse === undefined ? reply : state.tokenResponse, { status: state.tokenStatus });
53
+ }
54
+ if (state.apiReply) return state.apiReply(url, init);
55
+ assert.equal(init.method, "GET");
56
+ assert.equal(init.redirect, "error");
57
+ assert.equal(new Headers(init.headers).get("authorization"), `Bearer secret-${actor}-access-${state.tokenCount}`);
58
+ assert(["/api/conversations.list", "/api/auth.test"].includes(url.pathname));
59
+ return Response.json(url.pathname === "/api/auth.test" ? { ok: true, team_id: "T123", user_id: "U123" } : state.page, { status: state.apiStatus });
60
+ }
61
+ };
62
+ const service = createConnectionService(options);
63
+ async function start() {
64
+ const { authorizationUrl } = await service.beginAuthorization(input);
65
+ const url = new URL(authorizationUrl);
66
+ const redirect = new URL(callback);
67
+ redirect.searchParams.set("code", "fixture-code");
68
+ redirect.searchParams.set("state", url.searchParams.get("state"));
69
+ return { url, callbackUrl: redirect.href };
70
+ }
71
+ const connect = async () => service.completeAuthorization({ ...input, callbackUrl: (await start()).callbackUrl });
72
+ return { service, options, requests, state, start, connect, directory, protection };
73
+ }
74
+
75
+ for (const actor of ["user", "bot"]) {
76
+ test(`Slack ${actor}: consent selects only its identity and persists encrypted grants across restart`, async (t) => {
77
+ const { service, options, start, requests, directory, protection } = await fixture(t, actor);
78
+ const { url, callbackUrl } = await start();
79
+ assert.equal(url.origin + url.pathname, "https://slack.com/oauth/v2/authorize");
80
+ assert.equal(url.searchParams.get(actor === "user" ? "user_scope" : "scope"), scopes.join(","));
81
+ assert.equal(url.searchParams.has(actor === "user" ? "scope" : "user_scope"), false);
82
+ assert.equal(url.searchParams.get("redirect_uri"), callback);
83
+ assert.equal(url.searchParams.get("code_challenge_method"), "S256");
84
+ assert.equal(url.searchParams.has("client_secret"), false);
85
+ const connected = await service.completeAuthorization({ ...input, callbackUrl });
86
+ assert.equal(connected.status, "connected");
87
+ assert.deepEqual(connected.grantedScopes, scopes);
88
+ const body = new URLSearchParams(requests[0].init.body);
89
+ assert.equal(body.get("redirect_uri"), callback);
90
+ assert.equal(body.get("code"), "fixture-code");
91
+ assert(body.get("code_verifier"));
92
+ assert.equal(requests[1].url.searchParams.get("types"), "public_channel");
93
+ for (const filename of await readdir(directory)) assert(!(await readFile(path.join(directory, filename), "utf8")).includes("secret-"));
94
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
95
+ assert.deepEqual(await restarted.status(input), { ...connected, callbackUrl: callback });
96
+ for (const [operation, type] of [["channels.list", "public_channel"], ["groups.list", "private_channel"], ["directMessages.list", "im"], ["groupMessages.list", "mpim"]]) {
97
+ assert.deepEqual(await restarted.invoke({ ...input, operation, input: { limit: 5, cursor: "next +&/=", exclude_archived: true } }), page);
98
+ assert.equal(requests.at(-1).url.searchParams.get("types"), type);
99
+ assert.equal(requests.at(-1).url.searchParams.get("cursor"), "next +&/=");
100
+ assert.equal(requests.at(-1).url.searchParams.get("exclude_archived"), "true");
101
+ }
102
+ assert.deepEqual(await restarted.invoke({ ...input, operation: "auth.test" }), { ok: true, team_id: "T123", user_id: "U123" });
103
+ for (const owner of [{ ...context, applicationId: "other-app" }, { ...context, subjectId: "other-user" }]) {
104
+ await assert.rejects(restarted.invoke({ ...input, context: owner, operation: "channels.list" }), { code: "connector_reconnect_required" });
105
+ }
106
+ await restarted.disconnect(input);
107
+ assert.equal((await service.status(input)).status, "disconnected");
108
+ });
109
+
110
+ test(`Slack ${actor}: serializes rotating refresh tokens across services and retains its identity`, async (t) => {
111
+ const { service, options, state, connect, requests } = await fixture(t, actor);
112
+ await connect();
113
+ state.time += 40_000;
114
+ const restarted = createConnectionService(options);
115
+ await Promise.all([service.invoke({ ...input, operation: "channels.list" }), restarted.invoke({ ...input, operation: "groups.list" })]);
116
+ assert.equal(state.tokenCount, 2);
117
+ state.time += 40_000;
118
+ await restarted.invoke({ ...input, operation: "channels.list" });
119
+ assert.equal(state.tokenCount, 3);
120
+ assert(requests.filter(({ url }) => url.pathname === "/api/oauth.v2.access").every(({ init }) => !String(init.body).includes("secret-bot-refresh") || actor === "bot"));
121
+ });
122
+
123
+ test(`Slack ${actor}: non-expiring grants work without synthetic refresh tokens`, async (t) => {
124
+ const { service, state, connect } = await fixture(t, actor);
125
+ state.rotation = false;
126
+ await connect();
127
+ state.time += 24 * 60 * 60 * 1000;
128
+ await service.invoke({ ...input, operation: "channels.list" });
129
+ assert.equal(state.tokenCount, 1);
130
+ });
131
+
132
+ test(`Slack ${actor}: wrong-identity and malformed token replies cannot connect or borrow the other token`, async (t) => {
133
+ const { service, state, connect } = await fixture(t, actor);
134
+ for (const reply of [null, { ok: true }, { ok: true, token_type: "bot", access_token: "secret-bot", scope: "channels:read", authed_user: { token_type: "bot" } },
135
+ { ok: true, token_type: "user", scope: "channels:read", access_token: "secret-user", authed_user: { token_type: "user", scope: [] } }]) {
136
+ if (actor === "bot" && reply?.token_type === "bot") continue;
137
+ state.tokenResponse = reply;
138
+ await assert.rejects(connect(), { code: "connector_response_invalid" });
139
+ assert.equal((await service.status(input)).status, "disconnected");
140
+ }
141
+ state.tokenResponse = undefined;
142
+ state.tokenScope = "groups:read";
143
+ await assert.rejects(connect(), { code: "connector_scope_missing" });
144
+ assert.equal((await service.status(input)).status, "disconnected");
145
+ });
146
+ }
147
+
148
+ test("Slack configuration keeps 57 permission choices but rejects permissions belonging to the other actor", async (t) => {
149
+ const { options, requests } = await fixture(t);
150
+ assert.equal(slackProvider.scopes.length, 57);
151
+ assert.equal(slackProvider.scopesForSettings({ actor: "user" }).length, 52);
152
+ assert.equal(slackProvider.scopesForSettings({ actor: "bot" }).length, 49);
153
+ const parse = (config) => parseIntegrationConfiguration(JSON.stringify(config), { providers: [slackProvider] });
154
+ for (const actor of ["user", "bot"]) {
155
+ const config = structuredClone(options.configuration);
156
+ config.integrations.slack.settings.actor = actor;
157
+ config.integrations.slack.scopes = slackProvider.scopesForSettings({ actor }).map(({ value }) => value);
158
+ assert.deepEqual(parse(config).integrations.slack.scopes, config.integrations.slack.scopes);
159
+ config.integrations.slack.scopes = [actor === "user" ? "channels:join" : "users.profile:write"];
160
+ assert.throws(() => parse(config), (error) => Boolean(error.fieldErrors["integrations.slack.scopes"]));
161
+ }
162
+ const invalid = structuredClone(options.configuration);
163
+ invalid.integrations.slack.settings.actor = "admin";
164
+ assert.throws(() => parse(invalid));
165
+ delete invalid.integrations.slack.settings;
166
+ assert.equal(parse(invalid).integrations.slack.settings.actor, "user");
167
+ assert.equal(requests.length, 0);
168
+ });
169
+
170
+ test("Slack changing actor invalidates pending attempts and connections, and cancellation preserves existing access", async (t) => {
171
+ const { service, options, start, connect, requests } = await fixture(t);
172
+ await connect();
173
+ const pending = await start();
174
+ await service.cancelAuthorization({ ...input, state: pending.url.searchParams.get("state") });
175
+ await assert.rejects(service.completeAuthorization({ ...input, callbackUrl: pending.callbackUrl }), { code: "connector_attempt_invalid" });
176
+ assert.equal((await service.status(input)).status, "connected");
177
+ const next = await start();
178
+ const configuration = structuredClone(options.configuration);
179
+ configuration.integrations.slack.settings.actor = "bot";
180
+ const changed = createConnectionService({ ...options, configuration });
181
+ const before = requests.length;
182
+ await assert.rejects(changed.completeAuthorization({ ...input, callbackUrl: next.callbackUrl }), { code: "connector_attempt_invalid" });
183
+ await assert.rejects(changed.invoke({ ...input, operation: "channels.list" }), { code: "connector_reconnect_required" });
184
+ assert.equal((await changed.status(input)).status, "reconnect-required");
185
+ assert.equal(requests.length, before);
186
+ });
187
+
188
+ test("Slack applies operation permissions and input bounds before network access and preserves empty cursor pages", async (t) => {
189
+ const { service, state, connect, requests } = await fixture(t);
190
+ state.tokenScope = "channels:read";
191
+ await connect();
192
+ const before = requests.length;
193
+ await assert.rejects(service.invoke({ ...input, operation: "groups.list" }), { code: "connector_scope_missing" });
194
+ for (const value of [{ limit: 0 }, { limit: 201 }, { limit: 1.5 }, { types: "private_channel" }, { team_id: "TOTHER" }, { cursor: "" }, { url: "https://attacker.example" }]) {
195
+ await assert.rejects(service.invoke({ ...input, operation: "channels.list", input: value }), { code: "connector_input_invalid" });
196
+ }
197
+ assert.equal(requests.length, before);
198
+ state.page = { ok: true, channels: [], response_metadata: { next_cursor: "more" } };
199
+ assert.deepEqual(await service.invoke({ ...input, operation: "channels.list" }), state.page);
200
+ for (const reply of [{ ok: true, channels: null }, { ok: true, channels: [null] }, { ok: true, channels: [], response_metadata: [] }, { ok: true, channels: [], response_metadata: { next_cursor: 3 } }]) {
201
+ state.page = reply;
202
+ await assert.rejects(service.invoke({ ...input, operation: "channels.list" }), { code: "connector_response_invalid" });
203
+ }
204
+ });
205
+
206
+ test("Slack HTTP-200 failures and refresh failures stay errors without exposing provider fields", async (t) => {
207
+ const { service, state, connect } = await fixture(t);
208
+ await connect();
209
+ for (const [error, code] of [["missing_scope", "connector_permission_denied"], ["ratelimited", "connector_rate_limited"], ["unknown_error", "connector_provider_failed"]]) {
210
+ state.page = { ok: false, error, secret: "secret-client" };
211
+ await assert.rejects(service.invoke({ ...input, operation: "channels.list" }), (failure) => {
212
+ assert.equal(failure.code, code);
213
+ assert(!JSON.stringify(failure).includes("secret-client"));
214
+ return true;
215
+ });
216
+ }
217
+ state.page = page;
218
+ state.time += 40_000;
219
+ state.tokenResponse = { ok: false, error: "invalid_refresh_token", detail: "secret-client" };
220
+ await assert.rejects(service.invoke({ ...input, operation: "channels.list" }), { code: "connector_reconnect_required" });
221
+ assert.equal((await service.status(input)).status, "reconnect-required");
222
+ });
223
+
224
+ test("Slack propagates HTTP status, invalid registration and malformed token errors", async (t) => {
225
+ const { service, state, connect } = await fixture(t);
226
+ for (const [status, code] of [[401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"]]) {
227
+ state.tokenStatus = status;
228
+ state.tokenText = "secret-client";
229
+ await assert.rejects(connect(), { code });
230
+ assert.equal((await service.status(input)).status, "disconnected");
231
+ }
232
+ state.tokenStatus = 200;
233
+ await assert.rejects(connect(), { code: "connector_response_invalid" });
234
+ state.tokenText = undefined;
235
+ state.tokenResponse = { ok: false, error: "bad_client_secret" };
236
+ await assert.rejects(connect(), { code: "connector_registration_invalid" });
237
+ });
238
+
239
+ test("Slack API throttling does not replay and revocation persists reconnect state", async (t) => {
240
+ const { service, options, state, connect, requests } = await fixture(t);
241
+ await connect();
242
+ let before = requests.length;
243
+ state.apiStatus = 429;
244
+ await assert.rejects(service.invoke({ ...input, operation: "channels.list" }), { code: "connector_rate_limited" });
245
+ assert.equal(requests.length, before + 1);
246
+ assert.equal((await service.status(input)).status, "connected");
247
+ state.apiStatus = 200;
248
+ state.page = { ok: false, error: "token_revoked", detail: "secret-client" };
249
+ await assert.rejects(service.invoke({ ...input, operation: "channels.list" }), { code: "connector_reconnect_required" });
250
+ const restarted = createConnectionService(options);
251
+ assert.equal((await restarted.status(input)).status, "reconnect-required");
252
+ before = requests.length;
253
+ await assert.rejects(restarted.invoke({ ...input, operation: "channels.list" }), { code: "connector_reconnect_required" });
254
+ assert.equal(requests.length, before);
255
+ });
256
+
257
+ test("Slack interruption and timeout do not report a completed connection", async (t) => {
258
+ for (const [path, name, code] of [["/api/oauth.v2.access", "AbortError", "connector_cancelled"], ["/api/conversations.list", "TimeoutError", "connector_provider_timeout"]]) {
259
+ const { service, state, start } = await fixture(t);
260
+ const pending = await start();
261
+ state.controller = new AbortController();
262
+ state.interruptPath = path;
263
+ state.interruptName = name;
264
+ await assert.rejects(service.completeAuthorization({ ...input, callbackUrl: pending.callbackUrl, signal: state.controller.signal }), { code });
265
+ assert.equal((await service.status(input)).status, "disconnected");
266
+ }
267
+ });
268
+
269
+
270
+ test("Slack useful history, user lookup and message posting retain actor authorization", async (t) => {
271
+ for (const actor of ["user", "bot"]) {
272
+ const { service, state, requests, connect } = await fixture(t, actor, [...scopes, "channels:history", "users:read", "chat:write"]);
273
+ await connect();
274
+ state.apiReply = (url, init) => {
275
+ assert.equal(new Headers(init.headers).get("authorization"), `Bearer secret-${actor}-access-1`);
276
+ if (url.pathname === "/api/conversations.history") {
277
+ assert.equal(url.searchParams.get("channel"), "C123");
278
+ assert.equal(url.searchParams.get("cursor"), "next +&/=");
279
+ return Response.json({ ok: true, messages: [{ ts: "1700000000.000001", text: "Appointment tomorrow" }], has_more: false });
280
+ }
281
+ if (url.pathname === "/api/users.info") {
282
+ assert.equal(url.searchParams.get("user"), "U123");
283
+ return Response.json({ ok: true, user: { id: "U123", name: "reception" } });
284
+ }
285
+ assert.equal(url.pathname, "/api/chat.postMessage");
286
+ assert.equal(init.method, "POST");
287
+ assert.deepEqual(JSON.parse(init.body), { channel: "C123", text: "Appointment confirmed", thread_ts: "1700000000.000001", unfurl_links: false, unfurl_media: false });
288
+ return Response.json({ ok: true, channel: "C123", ts: "1700000001.000002", message: { text: "Appointment confirmed" } });
289
+ };
290
+ const history = await service.invoke({ ...input, operation: "channels.history", input: { channel: "C123", cursor: "next +&/=" } });
291
+ assert.equal(history.messages[0].text, "Appointment tomorrow");
292
+ assert.equal((await service.invoke({ ...input, operation: "users.info", input: { user: "U123" } })).user.name, "reception");
293
+ assert.equal((await service.invoke({ ...input, operation: "messages.send", input: { channel: "C123", text: "Appointment confirmed", thread_ts: "1700000000.000001" } })).ts, "1700000001.000002");
294
+ const before = requests.length;
295
+ await assert.rejects(service.invoke({ ...input, operation: "channels.history", input: { channel: "C123", limit: 16 } }));
296
+ await assert.rejects(service.invoke({ ...input, operation: "messages.send", input: { channel: "https://evil.test", text: "hello" } }));
297
+ await assert.rejects(service.invoke({ ...input, operation: "groups.history", input: { channel: "G123" } }));
298
+ assert.equal(requests.length, before);
299
+ }
300
+ });
301
+
302
+
303
+ test("Slack signed events and interactions reject tampering, stale requests and other installations", () => {
304
+ const base = { secret: "fixture-signing-secret", timestamp: "1700000000", now: 1700000000000, appId: "A123", teamId: "T123" };
305
+ const event = { type: "event_callback", api_app_id: "A123", team_id: "T123", event_id: "Ev123", event: { type: "message", text: "hello" } };
306
+ const signed = (payload, form = false) => {
307
+ const rawBody = Buffer.from(form ? new URLSearchParams({ payload: JSON.stringify(payload) }).toString() : JSON.stringify(payload));
308
+ return { ...base, rawBody, contentType: form ? "application/x-www-form-urlencoded" : "application/json", signature: "v0=" + createHmac("sha256", base.secret).update(`v0:${base.timestamp}:`).update(rawBody).digest("hex") };
309
+ };
310
+ assert.deepEqual(verifySlackRequest(signed(event)), event);
311
+ const interaction = { type: "block_actions", api_app_id: "A123", team: { id: "T123" }, user: { id: "U123" }, actions: [{ action_id: "confirm" }] };
312
+ assert.deepEqual(verifySlackRequest(signed(interaction, true)), interaction);
313
+ assert.equal(verifySlackRequest(signed({ type: "url_verification", challenge: "challenge" })).challenge, "challenge");
314
+ for (const request of [
315
+ { ...signed(event), rawBody: Buffer.from("{}") }, { ...signed(event), now: base.now + 301000 },
316
+ { ...signed(event), now: base.now - 301000 }, { ...signed(event), signature: "v0=no" },
317
+ signed({ ...event, team_id: "TOTHER" }), signed({ ...event, api_app_id: "AOTHER" }),
318
+ signed({ ...event, event_id: "" }), signed({ ...interaction, user: {} }, true)
319
+ ]) assert.throws(() => verifySlackRequest(request), { code: "connector_webhook_invalid" });
320
+ });