@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,167 @@
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 { githubApiProvider } from "../src/server/github-api.js";
9
+
10
+ for (const perUser of [false, true]) test(`GitHub OAuth ${perUser ? "per-user non-expiring" : "shared rotating"} grants preserve protocol and ownership`, async (t) => {
11
+ const directory = await mkdtemp(path.join(tmpdir(), "github-oauth-test-"));
12
+ t.after(() => rm(directory, { recursive: true, force: true }));
13
+ const callback = "https://example.test/integrations/github-api/callback";
14
+ const context = { applicationId: "app", subjectId: "owner" };
15
+ const input = { context, integrationId: "github" };
16
+ let time = Date.now(), exchanges = 0;
17
+ let tokenScope = "read:user,repo", apiStatus = 200;
18
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(4) }, activeKeyId: "current" });
19
+ const options = {
20
+ configuration: { schemaVersion: 1, registrations: { github: { source: "own", clientId: "client", clientSecretRef: "env:SECRET", callbackUrlRef: "env:CALLBACK", tokenEndpointAuthMethod: "client_secret_post" } }, integrations: {
21
+ github: { provider: "github-api", accountMode: perUser ? "per-user" : "shared", scopes: ["read:user", "repo"], authentication: { method: "oauth2", registrationRef: "github" } },
22
+ personal: { provider: "github-api", accountMode: "shared", scopes: ["read:user", "repo"], authentication: { method: "api-key", secretRef: "env:TOKEN" } }
23
+ } }, providers: [githubApiProvider], authorize: async owner => owner,
24
+ store: createFileConnectionStore({ directory, protection }), now: () => time,
25
+ resolveReference: async ref => ref === "env:CALLBACK" ? callback : ref === "env:TOKEN" ? "personal" : "secret",
26
+ fetchImpl: async (address, init) => {
27
+ const url = new URL(String(address)), headers = new Headers(init.headers);
28
+ if (url.origin === "https://github.com") {
29
+ assert.equal(url.pathname, "/login/oauth/access_token");
30
+ assert.equal(headers.get("accept"), "application/json");
31
+ const body = new URLSearchParams(init.body);
32
+ assert.equal(body.get("client_secret"), "secret");
33
+ if (body.get("grant_type") === "authorization_code") {
34
+ assert.equal(body.get("redirect_uri"), callback);
35
+ assert.ok(body.get("code_verifier"));
36
+ } else assert.equal(body.get("refresh_token"), `refresh-${exchanges}`);
37
+ exchanges++;
38
+ if (perUser) return Response.json({ access_token: `access-${exchanges}`, token_type: "Bearer", scope: tokenScope });
39
+ return Response.json({ access_token: `access-${exchanges}`, refresh_token: `refresh-${exchanges}`, token_type: "Bearer", expires_in: 60, scope: tokenScope });
40
+ }
41
+ assert.equal(url.origin, "https://api.github.com");
42
+ assert.ok([`Bearer access-${exchanges}`, "Bearer personal"].includes(headers.get("authorization")));
43
+ if (apiStatus !== 200) return Response.json({ message: "private provider detail" }, { status: apiStatus });
44
+ assert.equal(headers.get("x-github-api-version"), "2026-03-10");
45
+ assert.equal(headers.get("user-agent"), "jskit-connectors");
46
+ return Response.json({ id: 123, login: "fixture" });
47
+ }
48
+ };
49
+ const service = createConnectionService(options);
50
+ for (const outcome of ["cancel", "denied"]) {
51
+ const attempt = new URL((await service.beginAuthorization(input)).authorizationUrl);
52
+ const denied = new URL(callback);
53
+ denied.searchParams.set("state", attempt.searchParams.get("state"));
54
+ if (outcome === "cancel") {
55
+ await service.cancelAuthorization({ ...input, state: attempt.searchParams.get("state") });
56
+ denied.searchParams.set("code", "unused");
57
+ } else denied.searchParams.set("error", "access_denied");
58
+ await assert.rejects(service.completeAuthorization({ ...input, callbackUrl: denied.href }), {
59
+ code: outcome === "cancel" ? "connector_attempt_invalid" : "connector_consent_denied"
60
+ });
61
+ }
62
+ assert.equal(exchanges, 0);
63
+ const { authorizationUrl } = await service.beginAuthorization(input);
64
+ const url = new URL(authorizationUrl);
65
+ assert.equal(url.origin + url.pathname, "https://github.com/login/oauth/authorize");
66
+ assert.equal(url.searchParams.get("code_challenge_method"), "S256");
67
+ const returned = new URL(callback);
68
+ returned.searchParams.set("code", "code");
69
+ returned.searchParams.set("state", url.searchParams.get("state"));
70
+ assert.equal((await service.completeAuthorization({ ...input, callbackUrl: returned.href })).status, "connected");
71
+ await assert.rejects(service.completeAuthorization({ ...input, callbackUrl: returned.href }), { code: "connector_attempt_invalid" });
72
+ time += 40_000;
73
+ await Promise.all([service.invoke({ ...input, operation: "account.read" }), service.invoke({ ...input, operation: "account.read" })]);
74
+ assert.equal(exchanges, perUser ? 1 : 2);
75
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
76
+ time += 40_000;
77
+ await restarted.invoke({ ...input, operation: "account.read" });
78
+ assert.equal(exchanges, perUser ? 1 : 3);
79
+ assert.equal((await restarted.connectApiKey({ context, integrationId: "personal" })).status, "connected");
80
+ const otherApp = { context: { ...context, applicationId: "another-app" }, integrationId: "github" };
81
+ assert.equal((await restarted.status(otherApp)).status, "disconnected");
82
+ if (perUser) {
83
+ const otherUser = { context: { ...context, subjectId: "another-user" }, integrationId: "github" };
84
+ assert.equal((await restarted.status(otherUser)).status, "disconnected");
85
+ await assert.rejects(restarted.invoke({ ...otherUser, operation: "account.read" }));
86
+ assert.equal((await restarted.status(input)).status, "connected");
87
+ }
88
+ await restarted.disconnect(input);
89
+ assert.equal((await restarted.status(input)).status, "disconnected");
90
+ });
91
+
92
+ test("GitHub token normalization rejects malformed grants and retains OAuth error responses", async () => {
93
+ for (const scope of [null, ["repo"], "repo,,gist", "repo gist", { repo: true }]) {
94
+ await assert.rejects(githubApiProvider.normalizeTokenResponse(Response.json({ access_token: "private", token_type: "bearer", scope })), { code: "connector_response_invalid" });
95
+ }
96
+ const failure = Response.json({ error: "bad_verification_code" });
97
+ assert.equal(await githubApiProvider.normalizeTokenResponse(failure), failure);
98
+ const denied = new Response("denied", { status: 403 });
99
+ assert.equal(await githubApiProvider.normalizeTokenResponse(denied), denied);
100
+ for (const scope of ["", "read:user", "read:user,repo"]) {
101
+ const result = await githubApiProvider.normalizeTokenResponse(Response.json({ access_token: "private", token_type: "bearer", scope }));
102
+ assert.equal((await result.json()).scope, scope.replaceAll(",", " "));
103
+ }
104
+ });
105
+
106
+ test("GitHub repository reads and issue/PR writes preserve paths, pagination and uncertain outcomes", async t => {
107
+ const directory = await mkdtemp(path.join(tmpdir(), "github-content-test-"));
108
+ t.after(() => rm(directory, { recursive: true, force: true }));
109
+ const requests = []; let status = 200, malformed = false, fail = false;
110
+ const service = createConnectionService({
111
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { github: { provider: "github-api", accountMode: "shared", scopes: [], authentication: { method: "api-key", secretRef: "env:GITHUB_TOKEN" } } } },
112
+ providers: [githubApiProvider], authorize: async owner => owner, resolveReference: async () => "private-token",
113
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(8) }, activeKeyId: "current" }) }),
114
+ fetchImpl: async (url, init) => {
115
+ const parsed = new URL(url); requests.push({ url: parsed, init });
116
+ assert.equal(parsed.origin, "https://api.github.com");
117
+ assert.equal(new Headers(init.headers).get("authorization"), "Bearer private-token");
118
+ assert.equal(init.redirect, "error");
119
+ if (fail) throw new Error("outcome unknown");
120
+ if (status !== 200) return Response.json({ message: "private provider detail" }, { status });
121
+ if (malformed) return Response.json({});
122
+ const route = parsed.pathname;
123
+ if (route === "/user") return Response.json({ id: 1, login: "owner" });
124
+ if (route.endsWith("/contents/docs/read%20me.md")) return Response.json({ type: "file", path: "docs/read me.md", encoding: "base64", content: Buffer.from("Useful documentation").toString("base64") });
125
+ if (route.endsWith("/actions/runs")) return Response.json({ workflow_runs: [{ id: 2, conclusion: "success" }] });
126
+ if (route === "/repos/owner/project") return Response.json({ id: 1, full_name: "owner/project" });
127
+ if (init.method === "POST" || init.method === "PATCH") return Response.json({ number: 7, title: "Fix checkout", ...JSON.parse(init.body) });
128
+ if (/\/(issues|pulls)\/7$/.test(route)) return Response.json({ number: 7, title: "Fix checkout", body: "Customer report" });
129
+ return Response.json([{ id: 3, name: "main", body: "Review comment", sha: "revision" }]);
130
+ }
131
+ });
132
+ const owner = { context: { applicationId: "repo-app", subjectId: "owner" }, integrationId: "github" };
133
+ const invoke = (operation, input = {}) => service.invoke({ ...owner, operation, input: { owner: "owner", repo: "project", ...input } });
134
+ await service.connectApiKey(owner);
135
+ assert.equal((await invoke("repositories.get")).full_name, "owner/project");
136
+ const content = await invoke("contents.get", { path: "docs/read me.md", ref: "feature/docs" });
137
+ assert.equal(Buffer.from(content.content, content.encoding).toString(), "Useful documentation");
138
+ assert.equal(requests.at(-1).url.searchParams.get("ref"), "feature/docs");
139
+ for (const operation of ["branches.list", "commits.list", "releases.list", "issues.list", "pulls.list", "issues.comments", "pulls.reviews"]) {
140
+ const input = operation.endsWith("comments") || operation.endsWith("reviews") ? { number: 7 } : {};
141
+ assert.equal((await invoke(operation, { ...input, page: 2, per_page: 10 })).length, 1);
142
+ assert.equal(requests.at(-1).url.searchParams.get("page"), "2");
143
+ }
144
+ assert.equal((await invoke("workflows.runs")).workflow_runs[0].conclusion, "success");
145
+ for (const operation of ["issues.get", "pulls.get"]) assert.equal((await invoke(operation, { number: 7 })).body, "Customer report");
146
+ await invoke("issues.create", { title: "Fix checkout", body: "Customer report" });
147
+ assert.equal(requests.at(-1).init.method, "POST");
148
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { title: "Fix checkout", body: "Customer report" });
149
+ await invoke("pulls.create", { title: "Fix checkout", head: "fix/checkout", base: "main" });
150
+ assert.equal(JSON.parse(requests.at(-1).init.body).draft, false);
151
+ for (const operation of ["issues.update", "pulls.update"]) {
152
+ assert.equal((await invoke(operation, { number: 7, state: "closed" })).state, "closed");
153
+ assert.equal(requests.at(-1).init.method, "PATCH");
154
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { state: "closed" });
155
+ }
156
+ const before = requests.length;
157
+ for (const [operation, input] of [["contents.get", { path: "../secret" }], ["repositories.get", { owner: "other/owner" }], ["issues.list", { per_page: 101 }], ["issues.update", { number: 7 }], ["pulls.create", { title: "x", head: "x" }]]) await assert.rejects(invoke(operation, input));
158
+ assert.equal(requests.length, before);
159
+ malformed = true; await assert.rejects(invoke("issues.create", { title: "x" }), { code: "connector_response_invalid" }); malformed = false;
160
+ for (const [httpStatus, code] of [[403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) { status = httpStatus; await assert.rejects(invoke("issues.create", { title: "x" }), { code }); }
161
+ status = 200; fail = true; const failedBefore = requests.length;
162
+ await assert.rejects(invoke("pulls.create", { title: "x", head: "fix", base: "main" }));
163
+ assert.equal(requests.length, failedBefore + 1); fail = false;
164
+ await service.disconnect(owner); const disconnected = requests.length;
165
+ await assert.rejects(invoke("issues.get", { number: 7 })); assert.equal(requests.length, disconnected);
166
+ await service.connectApiKey(owner); assert.equal((await invoke("issues.get", { number: 7 })).number, 7);
167
+ });
@@ -0,0 +1,160 @@
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 { gitlabApiProvider } from "../src/server/gitlab-api.js";
9
+
10
+ for (const perUser of [false, true]) test(`GitLab OAuth ${perUser ? "per-user non-expiring" : "shared rotating"} grants preserve protocol and ownership`, async (t) => {
11
+ const directory = await mkdtemp(path.join(tmpdir(), "gitlab-oauth-test-"));
12
+ t.after(() => rm(directory, { recursive: true, force: true }));
13
+ const callback = "https://example.test/integrations/gitlab-api/callback";
14
+ const context = { applicationId: "app", subjectId: "owner" };
15
+ const input = { context, integrationId: "gitlab" };
16
+ let time = Date.now(), exchanges = 0;
17
+ let tokenScope = "read_user read_api", apiStatus = 200;
18
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(4) }, activeKeyId: "current" });
19
+ const options = {
20
+ configuration: { schemaVersion: 1, registrations: { gitlab: { source: "own", clientId: "client", clientSecretRef: "env:SECRET", callbackUrlRef: "env:CALLBACK", tokenEndpointAuthMethod: "client_secret_post" } }, integrations: {
21
+ gitlab: { provider: "gitlab-api", settings: { instanceUrl: "https://gitlab.example.test" }, accountMode: perUser ? "per-user" : "shared", scopes: ["read_user", "read_api"], authentication: { method: "oauth2", registrationRef: "gitlab" } },
22
+ personal: { provider: "gitlab-api", settings: { instanceUrl: "https://gitlab.example.test" }, accountMode: "shared", scopes: ["read_user", "read_api"], authentication: { method: "api-key", secretRef: "env:TOKEN" } }
23
+ } }, providers: [gitlabApiProvider], authorize: async owner => owner,
24
+ store: createFileConnectionStore({ directory, protection }), now: () => time,
25
+ resolveReference: async ref => ref === "env:CALLBACK" ? callback : ref === "env:TOKEN" ? "personal" : "secret",
26
+ fetchImpl: async (address, init) => {
27
+ const url = new URL(String(address)), headers = new Headers(init.headers);
28
+ assert.equal(url.origin, "https://gitlab.example.test");
29
+ if (url.pathname === "/oauth/token") {
30
+ assert.equal(url.pathname, "/oauth/token");
31
+ assert.equal(headers.get("accept"), "application/json");
32
+ const body = new URLSearchParams(init.body);
33
+ assert.equal(body.get("client_secret"), "secret");
34
+ if (body.get("grant_type") === "authorization_code") {
35
+ assert.equal(body.get("redirect_uri"), callback);
36
+ assert.ok(body.get("code_verifier"));
37
+ } else {
38
+ assert.equal(body.get("refresh_token"), `refresh-${exchanges}`);
39
+ assert.equal(body.get("redirect_uri"), callback);
40
+ }
41
+ exchanges++;
42
+ if (perUser) return Response.json({ access_token: `access-${exchanges}`, token_type: "Bearer", scope: tokenScope });
43
+ return Response.json({ access_token: `access-${exchanges}`, refresh_token: `refresh-${exchanges}`, token_type: "Bearer", expires_in: 60, scope: tokenScope });
44
+ }
45
+ assert.equal(url.origin, "https://gitlab.example.test");
46
+ assert.ok(headers.get("authorization") === `Bearer access-${exchanges}` || headers.get("private-token") === "personal");
47
+ if (apiStatus !== 200) return Response.json({ message: "private provider detail" }, { status: apiStatus });
48
+ return Response.json({ id: 123, username: "fixture" });
49
+ }
50
+ };
51
+ const service = createConnectionService(options);
52
+ for (const outcome of ["cancel", "denied"]) {
53
+ const attempt = new URL((await service.beginAuthorization(input)).authorizationUrl);
54
+ const denied = new URL(callback);
55
+ denied.searchParams.set("state", attempt.searchParams.get("state"));
56
+ if (outcome === "cancel") {
57
+ await service.cancelAuthorization({ ...input, state: attempt.searchParams.get("state") });
58
+ denied.searchParams.set("code", "unused");
59
+ } else denied.searchParams.set("error", "access_denied");
60
+ await assert.rejects(service.completeAuthorization({ ...input, callbackUrl: denied.href }), {
61
+ code: outcome === "cancel" ? "connector_attempt_invalid" : "connector_consent_denied"
62
+ });
63
+ }
64
+ assert.equal(exchanges, 0);
65
+ const { authorizationUrl } = await service.beginAuthorization(input);
66
+ const url = new URL(authorizationUrl);
67
+ assert.equal(url.origin + url.pathname, "https://gitlab.example.test/oauth/authorize");
68
+ assert.equal(url.searchParams.get("code_challenge_method"), "S256");
69
+ const returned = new URL(callback);
70
+ returned.searchParams.set("code", "code");
71
+ returned.searchParams.set("state", url.searchParams.get("state"));
72
+ assert.equal((await service.completeAuthorization({ ...input, callbackUrl: returned.href })).status, "connected");
73
+ await assert.rejects(service.completeAuthorization({ ...input, callbackUrl: returned.href }), { code: "connector_attempt_invalid" });
74
+ time += 40_000;
75
+ await Promise.all([service.invoke({ ...input, operation: "profile.read" }), service.invoke({ ...input, operation: "profile.read" })]);
76
+ assert.equal(exchanges, perUser ? 1 : 2);
77
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
78
+ time += 40_000;
79
+ await restarted.invoke({ ...input, operation: "profile.read" });
80
+ assert.equal(exchanges, perUser ? 1 : 3);
81
+ assert.equal((await restarted.connectApiKey({ context, integrationId: "personal" })).status, "connected");
82
+ const otherApp = { context: { ...context, applicationId: "another-app" }, integrationId: "gitlab" };
83
+ assert.equal((await restarted.status(otherApp)).status, "disconnected");
84
+ if (perUser) {
85
+ const otherUser = { context: { ...context, subjectId: "another-user" }, integrationId: "gitlab" };
86
+ assert.equal((await restarted.status(otherUser)).status, "disconnected");
87
+ await assert.rejects(restarted.invoke({ ...otherUser, operation: "profile.read" }));
88
+ assert.equal((await restarted.status(input)).status, "connected");
89
+ }
90
+ const changedConfiguration = structuredClone(options.configuration);
91
+ changedConfiguration.integrations.gitlab.settings.instanceUrl = "https://another-gitlab.example.test";
92
+ const changed = createConnectionService({ ...options, configuration: changedConfiguration });
93
+ assert.equal((await changed.status(input)).status, "reconnect-required");
94
+ await assert.rejects(changed.invoke({ ...input, operation: "profile.read" }), { code: "connector_reconnect_required" });
95
+ for (const instanceUrl of ["http://gitlab.example.test", "https://name:secret@gitlab.example.test", "https://gitlab.example.test/subpath", "https://gitlab.example.test/?token=secret", "not-a-url"]) {
96
+ changedConfiguration.integrations.gitlab.settings.instanceUrl = instanceUrl;
97
+ assert.throws(() => createConnectionService({ ...options, configuration: changedConfiguration }), { code: "integration_configuration_invalid" });
98
+ }
99
+ await restarted.disconnect(input);
100
+ assert.equal((await restarted.status(input)).status, "disconnected");
101
+ });
102
+
103
+
104
+ test("GitLab project paths, content and issue/MR writes use the selected host without replaying writes", async t => {
105
+ const directory = await mkdtemp(path.join(tmpdir(), "gitlab-content-test-"));
106
+ t.after(() => rm(directory, { recursive: true, force: true }));
107
+ const requests = []; let status = 200, fail = false;
108
+ const service = createConnectionService({
109
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { gitlab: { provider: "gitlab-api", settings: { instanceUrl: "https://code.example.test" }, accountMode: "shared", scopes: [], authentication: { method: "api-key", secretRef: "env:GITLAB_TOKEN" } } } },
110
+ providers: [gitlabApiProvider], authorize: async owner => owner, resolveReference: async () => "private-token",
111
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(8) }, activeKeyId: "current" }) }),
112
+ fetchImpl: async (url, init) => {
113
+ const parsed = new URL(url); requests.push({ url: parsed, init });
114
+ assert.equal(parsed.origin, "https://code.example.test");
115
+ assert.equal(new Headers(init.headers).get("private-token"), "private-token");
116
+ assert.equal(init.redirect, "error");
117
+ if (fail) throw new Error("outcome unknown");
118
+ if (status !== 200) return Response.json({ message: "private detail" }, { status });
119
+ if (parsed.pathname === "/api/v4/user") return Response.json({ id: 1, username: "owner" });
120
+ assert.ok(parsed.pathname.startsWith("/api/v4/projects/team%2Fsubgroup%2Fproject"));
121
+ if (parsed.pathname.endsWith("/repository/files/docs%2Fread%20me.md")) return Response.json({ file_path: "docs/read me.md", encoding: "base64", content: Buffer.from("Useful documentation").toString("base64") });
122
+ if (parsed.pathname.endsWith("team%2Fsubgroup%2Fproject")) return Response.json({ id: 1, path_with_namespace: "team/subgroup/project" });
123
+ if (init.method !== "GET") return Response.json({ iid: 7, title: "Fix checkout", ...JSON.parse(init.body) });
124
+ if (/\/(issues|merge_requests)\/7$/.test(parsed.pathname)) return Response.json({ iid: 7, title: "Fix checkout", description: "Customer report" });
125
+ return Response.json([{ id: 3, body: "Review comment", status: "success" }]);
126
+ }
127
+ });
128
+ const owner = { context: { applicationId: "repo-app", subjectId: "owner" }, integrationId: "gitlab" };
129
+ const invoke = (operation, input = {}) => service.invoke({ ...owner, operation, input: { project: "team/subgroup/project", ...input } });
130
+ await service.connectApiKey(owner);
131
+ assert.equal((await invoke("projects.get")).path_with_namespace, "team/subgroup/project");
132
+ const file = await invoke("files.get", { filePath: "docs/read me.md", ref: "feature/docs" });
133
+ assert.equal(Buffer.from(file.content, file.encoding).toString(), "Useful documentation");
134
+ assert.equal(requests.at(-1).url.searchParams.get("ref"), "feature/docs");
135
+ for (const operation of ["branches.list", "commits.list", "pipelines.list", "issues.list", "mergeRequests.list", "issues.notes", "mergeRequests.notes"]) {
136
+ assert.equal((await invoke(operation, { ...(operation.endsWith("notes") ? { iid: 7 } : {}), page: 2, per_page: 10 })).length, 1);
137
+ assert.equal(requests.at(-1).url.searchParams.get("page"), "2");
138
+ }
139
+ for (const operation of ["issues.get", "mergeRequests.get"]) assert.equal((await invoke(operation, { iid: 7 })).description, "Customer report");
140
+ await invoke("issues.create", { title: "Fix checkout", description: "Customer report" });
141
+ assert.equal(requests.at(-1).url.pathname, "/api/v4/projects/team%2Fsubgroup%2Fproject/issues");
142
+ assert.equal(requests.at(-1).init.method, "POST");
143
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { title: "Fix checkout", description: "Customer report" });
144
+ await invoke("mergeRequests.create", { title: "Fix checkout", source_branch: "fix/checkout", target_branch: "main" });
145
+ assert.equal(JSON.parse(requests.at(-1).init.body).source_branch, "fix/checkout");
146
+ for (const operation of ["issues.update", "mergeRequests.update"]) {
147
+ assert.equal((await invoke(operation, { iid: 7, state_event: "close" })).state_event, "close");
148
+ assert.equal(requests.at(-1).init.method, "PUT");
149
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { state_event: "close" });
150
+ }
151
+ const before = requests.length;
152
+ for (const [operation, input] of [["files.get", { filePath: "../secret", ref: "main" }], ["projects.get", { project: "team/../another" }], ["issues.list", { per_page: 101 }], ["issues.update", { iid: 7 }], ["mergeRequests.create", { title: "x", source_branch: "fix" }]]) await assert.rejects(invoke(operation, input));
153
+ assert.equal(requests.length, before);
154
+ for (const [httpStatus, code] of [[403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) { status = httpStatus; await assert.rejects(invoke("issues.create", { title: "x" }), { code }); }
155
+ status = 200; fail = true; const failedBefore = requests.length;
156
+ await assert.rejects(invoke("issues.create", { title: "x" })); assert.equal(requests.length, failedBefore + 1); fail = false;
157
+ await service.disconnect(owner); const disconnected = requests.length;
158
+ await assert.rejects(invoke("issues.get", { iid: 7 })); assert.equal(requests.length, disconnected);
159
+ await service.connectApiKey(owner); assert.equal((await invoke("issues.get", { iid: 7 })).iid, 7);
160
+ });
@@ -0,0 +1,192 @@
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 { gongProvider } from "../src/server/gong.js";
10
+
11
+ const context = { applicationId: "sales-app", subjectId: "sales-team" };
12
+ const input = { context, integrationId: "sales" };
13
+ const users = { requestId: "fixture-request", users: [{ id: "123", emailAddress: "user@example.test" }],
14
+ records: { totalRecords: 1, currentPageSize: 1, currentPageNumber: 0 } };
15
+
16
+ async function fixture(t, apiBaseUrl = "https://api.gong.io") {
17
+ const directory = await mkdtemp(path.join(tmpdir(), "gong-connector-"));
18
+ t.after(() => rm(directory, { recursive: true, force: true }));
19
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(5) }, activeKeyId: "current" });
20
+ const requests = [];
21
+ const state = { secret: "fixture-access-secret", status: 200, response: users };
22
+ const configuration = { schemaVersion: 1, registrations: {}, integrations: { sales: {
23
+ provider: "gong", accountMode: "shared", scopes: [],
24
+ authentication: { method: "api-key", secretRef: "env:GONG_ACCESS_SECRET" },
25
+ settings: { accessKey: "EXAMPLE_ACCESS_KEY", apiBaseUrl }, extensions: { keep: true }
26
+ } } };
27
+ const options = {
28
+ configuration, providers: [gongProvider], authorize: async (owner) => owner,
29
+ store: createFileConnectionStore({ directory, protection }),
30
+ resolveReference: async (reference) => { assert.equal(reference, "env:GONG_ACCESS_SECRET"); return state.secret; },
31
+ fetchImpl: async (address, init) => {
32
+ requests.push({ url: new URL(String(address)), init, headers: new Headers(init.headers) });
33
+ return Response.json(state.response, { status: state.status });
34
+ }
35
+ };
36
+ return { service: createConnectionService(options), options, requests, directory, protection, state };
37
+ }
38
+
39
+ for (const apiBaseUrl of ["https://api.gong.io", "https://company-17.api.gong.io/"]) {
40
+ test(`Gong verifies ${apiBaseUrl} and persists ownership across restart and key rotation`, async (t) => {
41
+ const { service, options, requests, directory, protection, state } = await fixture(t, apiBaseUrl);
42
+ const connected = await service.connectApiKey(input);
43
+ assert.equal(connected.status, "connected");
44
+ assert.equal(requests[0].url.href, `${new URL(apiBaseUrl).origin}/v2/users?includeAvatars=false`);
45
+ assert.equal(requests[0].init.method, "GET");
46
+ assert.equal(requests[0].init.redirect, "error");
47
+ const header = `Basic ${Buffer.from(`EXAMPLE_ACCESS_KEY:${state.secret}`).toString("base64")}`;
48
+ assert.equal(requests[0].headers.get("authorization"), header);
49
+ assert.equal(requests[0].url.href.includes(state.secret), false);
50
+ for (const name of await readdir(directory)) {
51
+ const stored = await readFile(path.join(directory, name), "utf8");
52
+ assert.equal(stored.includes(state.secret), false);
53
+ assert.equal(stored.includes(header), false);
54
+ }
55
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
56
+ assert.deepEqual(await restarted.status(input), connected);
57
+ state.secret = "rotated-fixture-secret";
58
+ assert.deepEqual(await restarted.invoke({ ...input, operation: "users.list" }), users);
59
+ assert.equal(requests.at(-1).headers.get("authorization"), `Basic ${Buffer.from(`EXAMPLE_ACCESS_KEY:${state.secret}`).toString("base64")}`);
60
+ for (const owner of [{ ...context, applicationId: "other-app" }, { ...context, subjectId: "other-team" }]) {
61
+ await assert.rejects(restarted.invoke({ ...input, context: owner, operation: "users.list" }), { code: "connector_reconnect_required" });
62
+ }
63
+ assert.equal(requests.length, 2);
64
+ await restarted.disconnect(input);
65
+ assert.equal((await service.status(input)).status, "disconnected");
66
+ });
67
+ }
68
+
69
+ test("Gong rejects invalid key identities and untrusted URLs before network or credential access", async (t) => {
70
+ const { options, requests } = await fixture(t);
71
+ const parse = (configuration) => parseIntegrationConfiguration(JSON.stringify(configuration), { providers: [gongProvider] });
72
+ const configuration = structuredClone(options.configuration);
73
+ delete configuration.integrations.sales.settings.apiBaseUrl;
74
+ assert.deepEqual(parse(configuration), options.configuration);
75
+ configuration.integrations.sales.settings.accessKey = " EXAMPLE_ACCESS_KEY ";
76
+ assert.deepEqual(parse(configuration), options.configuration);
77
+ const invalidUrls = [
78
+ "https://api.gong.io.attacker.invalid", "https://api.gong.io@attacker.invalid", "http://api.gong.io",
79
+ "https://user:password@api.gong.io", "https://api.gong.io:444", "https://api.gong.io/v2",
80
+ "https://api.gong.io?url=elsewhere", "https://api.gong.io#fragment", "https://127.0.0.1",
81
+ "https://api.gong.io/../", "https://-bad.api.gong.io", "https://bad-.api.gong.io",
82
+ `https://${"a".repeat(64)}.api.gong.io`, "https://app.gong.io", "https://api.gong.io\\@attacker.invalid"
83
+ ];
84
+ for (const url of invalidUrls) {
85
+ const changed = structuredClone(options.configuration);
86
+ changed.integrations.sales.settings.apiBaseUrl = url;
87
+ assert.throws(() => parse(changed), (error) => Boolean(error.fieldErrors["integrations.sales.settings.apiBaseUrl"]));
88
+ }
89
+ for (const accessKey of [undefined, "", "user:password", "key with spaces", "key\nheader", "key\u0000", "x".repeat(513)]) {
90
+ const changed = structuredClone(options.configuration);
91
+ changed.integrations.sales.settings.accessKey = accessKey;
92
+ assert.throws(() => parse(changed), (error) => Boolean(error.fieldErrors["integrations.sales.settings.accessKey"]));
93
+ }
94
+ configuration.integrations.sales.authentication.secretRef = "raw-secret";
95
+ assert.throws(() => parse(configuration), (error) => Boolean(error.fieldErrors["integrations.sales.authentication.secretRef"]));
96
+ assert.equal(requests.length, 0);
97
+ });
98
+
99
+ test("Gong requires verification after key identity or host changes and never crosses customer origins", async (t) => {
100
+ const { service, options, requests } = await fixture(t, "https://company-17.api.gong.io");
101
+ await service.connectApiKey(input);
102
+ for (const [field, value] of [["accessKey", "OTHER_KEY"], ["apiBaseUrl", "https://company-18.api.gong.io"]]) {
103
+ const configuration = structuredClone(options.configuration);
104
+ configuration.integrations.sales.settings[field] = value;
105
+ const changed = createConnectionService({ ...options, configuration });
106
+ assert.equal((await changed.status(input)).status, "reconnect-required");
107
+ await assert.rejects(changed.invoke({ ...input, operation: "users.list" }), { code: "connector_reconnect_required" });
108
+ }
109
+ for (const origin of ["https://company-18.api.gong.io", "https://api.gong.io"]) {
110
+ const wrongHost = createConnectionService({ ...options, providers: [{ ...gongProvider, operations: {
111
+ "users.list": { scopes: [], request: () => ({ method: "GET", url: `${origin}/v2/users` }) }
112
+ } }] });
113
+ await assert.rejects(wrongHost.invoke({ ...input, operation: "users.list" }), { code: "connector_destination_invalid" });
114
+ }
115
+ assert.equal(requests.length, 1);
116
+ });
117
+
118
+ test("Gong preserves paged users and encodes opaque cursors with explicit avatar selection", async (t) => {
119
+ const { service, requests, state } = await fixture(t);
120
+ await service.connectApiKey(input);
121
+ state.response = { ...users, records: { ...users.records, totalRecords: 2, cursor: "next+&=token" } };
122
+ assert.deepEqual(await service.invoke({ ...input, operation: "users.list", input: { cursor: "next+&=token", includeAvatars: true } }), state.response);
123
+ assert.deepEqual(Object.fromEntries(requests.at(-1).url.searchParams), { cursor: "next+&=token", includeAvatars: "true" });
124
+ assert.equal(requests.length, 2);
125
+ for (const invalid of [{ cursor: "" }, { cursor: "x".repeat(8193) }, { includeAvatars: "perhaps" }, { page: 2 }, { apiBaseUrl: "https://company-18.api.gong.io" }, { url: "https://attacker.invalid" }]) {
126
+ await assert.rejects(service.invoke({ ...input, operation: "users.list", input: invalid }), { code: "connector_input_invalid" });
127
+ }
128
+ assert.equal(requests.length, 2);
129
+ state.response = { users: [], records: { totalRecords: 0, currentPageSize: 0, currentPageNumber: 0 } };
130
+ assert.deepEqual(await service.invoke({ ...input, operation: "users.list" }), state.response);
131
+ });
132
+
133
+ test("Gong rejects denied credentials, rate limits and malformed results without leaking provider bodies", async (t) => {
134
+ const { service, requests, state } = await fixture(t);
135
+ for (const [status, code] of [[200, "connector_response_invalid"], [401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
136
+ state.status = status;
137
+ state.response = { errors: [state.secret] };
138
+ await assert.rejects(service.connectApiKey(input), (error) => {
139
+ assert.equal(error.code, code);
140
+ assert.equal(error.message.includes(state.secret), false);
141
+ assert.equal(JSON.stringify(error).includes(state.secret), false);
142
+ return true;
143
+ });
144
+ assert.equal((await service.status(input)).status, "disconnected");
145
+ }
146
+ assert.equal(requests.length, 5);
147
+ state.status = 200;
148
+ state.response = users;
149
+ await service.connectApiKey(input);
150
+ for (const malformed of [{ users: [] }, { ...users, users: {} }, { ...users, records: { ...users.records, cursor: {} } }, { ...users, records: { ...users.records, totalRecords: -1 } }]) {
151
+ state.response = malformed;
152
+ await assert.rejects(service.invoke({ ...input, operation: "users.list" }), { code: "connector_response_invalid" });
153
+ }
154
+ state.status = 401;
155
+ await assert.rejects(service.invoke({ ...input, operation: "users.list" }), { code: "connector_reconnect_required" });
156
+ assert.equal((await service.status(input)).status, "reconnect-required");
157
+ });
158
+
159
+ test("Gong call content preserves speaker, CRM context, statistics and opaque paging without fetching media", async t => {
160
+ const { service, state, requests } = await fixture(t, "https://company-17.api.gong.io");
161
+ await service.connectApiKey(input);
162
+ const callId = "7782342274025937895";
163
+ state.response = { records: { ...users.records, cursor: " next +/= " }, calls: [{ metaData: { id: callId, title: "Renewal" },
164
+ parties: [{ speakerId: "22", name: "Customer" }], context: [{ system: "Salesforce", objects: [{ objectType: "Opportunity", objectId: "opp-one" }] }],
165
+ content: { brief: "Renewal next month" }, interaction: { personInteractionStats: [{ name: "Talk Ratio", value: 0.4 }] } }] };
166
+ const call = await service.invoke({ ...input, operation: "calls.extensive", input: { callIds: [callId], cursor: " previous +/= " } });
167
+ assert.equal(call.calls[0].context[0].objects[0].objectId, "opp-one");
168
+ const body = JSON.parse(requests.at(-1).init.body);
169
+ assert.deepEqual(body.filter, { callIds: [callId] });
170
+ assert.equal(body.cursor, " previous +/= ");
171
+ assert.equal(body.contentSelector.exposedFields.media, false);
172
+ assert.deepEqual(body.contentSelector.contextTiming, ["Now", "TimeOfCall"]);
173
+ assert.equal(requests.at(-1).url.origin, "https://company-17.api.gong.io");
174
+ state.response = { records: users.records, callTranscripts: [{ callId, transcript: [{ speakerId: "22", sentences: [{ start: 1000, end: 2000, text: "Renew next month" }] }] }] };
175
+ const transcript = await service.invoke({ ...input, operation: "calls.transcripts", input: { callIds: [callId] } });
176
+ assert.equal(transcript.callTranscripts[0].transcript[0].speakerId, call.calls[0].parties[0].speakerId);
177
+ assert.equal(transcript.callTranscripts[0].transcript[0].sentences[0].text, "Renew next month");
178
+ assert.equal(requests.at(-1).url.pathname, "/v2/calls/transcript");
179
+ state.response = { records: users.records, peopleInteractionStats: [{ userId: "33", personInteractionStats: [{ name: "Interactivity", value: 9.23 }] }] };
180
+ assert.equal((await service.invoke({ ...input, operation: "stats.interaction", input: { fromDate: "2026-09-01", toDate: "2026-09-03", userIds: ["33"] } })).peopleInteractionStats[0].personInteractionStats[0].value, 9.23);
181
+ state.response = { records: users.records, calls: [{ metaData: { id: callId }, media: { audioUrl: "https://media.example.test/temporary" } }] };
182
+ await service.invoke({ ...input, operation: "calls.extensive", input: { fromDateTime: "2026-09-01T00:00:00Z", toDateTime: "2026-09-03T00:00:00Z", includeMedia: true } });
183
+ assert.equal(JSON.parse(requests.at(-1).init.body).contentSelector.exposedFields.media, true);
184
+ assert.equal(requests.some(request => request.url.origin === "https://media.example.test"), false);
185
+ const count = requests.length;
186
+ for (const [operation, values] of [["calls.extensive", {}], ["calls.transcripts", { callIds: [Number(callId)] }], ["calls.transcripts", { fromDateTime: "2026-09-01T00:00:00Z" }], ["stats.interaction", { fromDate: "2026-02-30", toDate: "2026-03-01" }], ["stats.interaction", { fromDate: "2026-09-03", toDate: "2026-09-01" }]]) await assert.rejects(service.invoke({ ...input, operation, input: values }), { code: "connector_input_invalid" });
187
+ assert.equal(requests.length, count);
188
+ state.response = { records: { totalRecords: 0, currentPageSize: 0, currentPageNumber: 0 }, callTranscripts: [] };
189
+ assert.deepEqual((await service.invoke({ ...input, operation: "calls.transcripts", input: { callIds: [callId] } })).callTranscripts, []);
190
+ state.response = { callTranscripts: [] };
191
+ await assert.rejects(service.invoke({ ...input, operation: "calls.transcripts", input: { callIds: [callId] } }), { code: "connector_response_invalid" });
192
+ });