@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,101 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { paddleProvider } from "../src/server/paddle.js";
4
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
5
+
6
+ test("Paddle maps reads to the captured permission and requires exact host approval without executing payments", async () => {
7
+ let approved = false;
8
+ const decisions = [];
9
+ const options = {
10
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { paddle: {
11
+ provider: "paddle", accountMode: "shared", scopes: [], settings: { environment: "sandbox" },
12
+ authentication: { method: "api-key", secretRef: "env:PADDLE_API_KEY" },
13
+ assistantPolicy: { enabled: true, defaultPermission: "ask", actions: { "api.read": "never" } }
14
+ } } },
15
+ providers: [paddleProvider], executionMode: "assistant",
16
+ store: { withConnection: () => assert.fail("No grant access is needed for rejected or host-only actions") },
17
+ resolveReference: () => assert.fail("No credential is needed for policy checks"),
18
+ fetchImpl: () => assert.fail("Authorization must not execute a provider payment action"),
19
+ authorize: async (context, decision) => { decisions.push(decision); return { ...context, approved }; }
20
+ };
21
+ const runtime = createConnectionService(options);
22
+ const input = { context: { applicationId: "app", subjectId: "owner" }, integrationId: "paddle" };
23
+ await assert.rejects(runtime.invoke({ ...input, operation: "products.list", input: {} }), { code: "connector_access_denied" });
24
+ assert.equal(decisions.length, 0);
25
+ const action = { ...input, action: "products.create", input: { name: "Example", price: "1200", currency: "USD" } };
26
+ await assert.rejects(runtime.authorizeAssistantAction(action), { code: "connector_approval_required" });
27
+ assert.deepEqual(decisions[0].input, action.input);
28
+ assert.deepEqual(decisions[0].assistantPermission, { action: "products.create", decision: "ask" });
29
+ approved = true;
30
+ await runtime.authorizeAssistantAction(action);
31
+ await assert.rejects(runtime.authorizeAssistantAction({ ...action, action: "arbitrary.write" }), { code: "connector_operation_unknown" });
32
+ await assert.rejects(runtime.invoke({ ...input, operation: "unimplemented.payment", input: action.input }), { code: "connector_operation_unknown" });
33
+ });
34
+
35
+ test("Paddle product and recurring-price writes validate inputs and do not retry failures", async (t) => {
36
+ const { mkdtemp, rm } = await import("node:fs/promises");
37
+ const { tmpdir } = await import("node:os");
38
+ const { join } = await import("node:path");
39
+ const { createFileConnectionStore, createCredentialProtection } = await import("../../connectors-core/src/server/fileStorage.js");
40
+ const directory = await mkdtemp(join(tmpdir(), "paddle-writes-"));
41
+ t.after(() => rm(directory, { recursive: true, force: true }));
42
+ const writes = [];
43
+ let fail = false;
44
+ const productId = `pro_${"a".repeat(26)}`;
45
+ const priceId = `pri_${"b".repeat(26)}`;
46
+ const webhookId = `ntfset_${"c".repeat(26)}`;
47
+ const webhookWrites = [];
48
+ const runtime = createConnectionService({
49
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { paddle: {
50
+ provider: "paddle", accountMode: "shared", scopes: [], settings: { environment: "sandbox" },
51
+ authentication: { method: "api-key", secretRef: "env:PADDLE_API_KEY" }
52
+ } } }, providers: [paddleProvider], authorize: async (owner) => owner,
53
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(5) }, activeKeyId: "current" }) }),
54
+ resolveReference: async () => "private-key",
55
+ fetchImpl: async (address, init) => {
56
+ const url = new URL(address);
57
+ assert.equal(url.origin, "https://sandbox-api.paddle.com");
58
+ assert.equal(new Headers(init.headers).get("authorization"), "Bearer private-key");
59
+ if (url.pathname.startsWith("/checkout-domains/")) {
60
+ assert.equal(init.method, "GET");
61
+ return Response.json({ data: { id: `chedom_${"d".repeat(26)}`, domain: "app.example.com", status: "action_required" } });
62
+ }
63
+ if (init.method === "GET") return Response.json({ data: [], meta: { pagination: { has_more: false } } });
64
+ if (init.method === "PATCH") {
65
+ assert.equal(url.pathname, `/notification-settings/${webhookId}`);
66
+ webhookWrites.push(JSON.parse(init.body));
67
+ return Response.json({ data: { id: webhookId, active: false, endpoint_secret_key: "private-signing-secret" } });
68
+ }
69
+ assert.equal(init.method, "POST");
70
+ writes.push({ path: url.pathname, body: JSON.parse(init.body) });
71
+ if (fail) return Response.json({ error: "private-key" }, { status: 500 });
72
+ return Response.json({ data: { id: url.pathname === "/products" ? productId : priceId } }, { status: 201 });
73
+ }
74
+ });
75
+ const input = { context: { applicationId: "app", subjectId: "owner" }, integrationId: "paddle" };
76
+ await runtime.connectApiKey(input);
77
+ const domain = await runtime.invoke({ ...input, operation: "checkoutDomains.get", input: { domain_id: `chedom_${"d".repeat(26)}` } });
78
+ assert.equal(domain.data.status, "action_required");
79
+ await assert.rejects(runtime.invoke({ ...input, operation: "checkoutDomains.get", input: { domain_id: "../../escape" } }), { code: "connector_input_invalid" });
80
+ const product = { name: "Example", tax_category: "saas" };
81
+ await runtime.invoke({ ...input, operation: "products.create", input: product });
82
+ const price = { product_id: productId, description: "Monthly subscription", unit_price: { amount: "1200", currency_code: "USD" }, billing_cycle: { interval: "month", frequency: 1 } };
83
+ await runtime.invoke({ ...input, operation: "prices.create", input: price });
84
+ assert.deepEqual(writes, [{ path: "/products", body: product }, { path: "/prices", body: price }]);
85
+ await assert.rejects(runtime.invoke({ ...input, operation: "prices.create", input: { ...price, unit_price: { amount: "12.50", currency_code: "USD" } } }));
86
+ await assert.rejects(runtime.invoke({ ...input, operation: "products.create", input: { name: "Missing tax category" } }));
87
+ assert.equal(writes.length, 2);
88
+ const webhook = await runtime.invoke({ ...input, operation: "webhooks.update", input: {
89
+ notification_setting_id: webhookId, active: false, subscribed_events: ["transaction.completed"], traffic_source: "simulation"
90
+ } });
91
+ assert.deepEqual(webhookWrites, [{ active: false, subscribed_events: ["transaction.completed"], traffic_source: "simulation" }]);
92
+ assert.equal(JSON.stringify(webhook).includes("private-signing-secret"), false);
93
+ await assert.rejects(runtime.invoke({ ...input, operation: "webhooks.update", input: { notification_setting_id: webhookId } }), { code: "connector_input_invalid" });
94
+ await assert.rejects(runtime.invoke({ ...input, operation: "webhooks.update", input: { notification_setting_id: "../../escape", active: false } }), { code: "connector_input_invalid" });
95
+ assert.equal(webhookWrites.length, 1);
96
+ fail = true;
97
+ await assert.rejects(runtime.invoke({ ...input, operation: "products.create", input: product }), (error) => {
98
+ assert.equal(error.message.includes("private-key"), false); return true;
99
+ });
100
+ assert.equal(writes.length, 3);
101
+ });
@@ -0,0 +1,39 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createPerplexityAnswers } from "../patterns/ai-connections/example/perplexity-answer.js";
4
+
5
+ const config = { schemaVersion: 1, registrations: {}, integrations: { research: {
6
+ provider: "perplexity", accountMode: "shared", scopes: [], authentication: { method: "api-key", secretRef: "env:RESEARCH_KEY" }
7
+ } } };
8
+ const input = { context: { applicationId: "app", subjectId: "user" }, integrationId: "research", model: "sonar", question: "What does the source say?" };
9
+ test("Perplexity composition uses the saved slot for cited answers, streaming, cancellation and key rotation", async () => {
10
+ let key = "fixture-first"; const calls = [];
11
+ const result = { choices: [{ message: { content: "Answer [1]" } }], citations: ["https://example.test/source"], search_results: [{ url: "https://example.test/source" }], usage: { total_tokens: 10 } };
12
+ const chunks = [{ choices: [{ delta: { content: "Answer" } }] }, { choices: [], citations: result.citations }];
13
+ const answer = createPerplexityAnswers({ configuration: config, authorize: async owner => owner,
14
+ resolveReference: async (ref, owner, slot) => { assert.equal(ref, "env:RESEARCH_KEY"); assert.equal(owner.applicationId, "app"); assert.equal(slot.integrationId, "research"); return key; },
15
+ createClient: options => ({ chat: { completions: { create: async (body, request) => {
16
+ calls.push({ options, body, request });
17
+ return body.stream ? (async function* () { yield* chunks; })() : result;
18
+ } } } }) });
19
+ assert.deepEqual(await answer(input), result);
20
+ assert.equal(calls[0].options.apiKey, key); assert.equal(calls[0].options.baseURL, "https://api.perplexity.ai"); assert.equal(calls[0].options.maxRetries, 0);
21
+ assert.deepEqual(calls[0].body.messages, [{ role: "user", content: input.question }]);
22
+ key = "fixture-rotated"; const controller = new AbortController(); const received = [];
23
+ for await (const chunk of await answer({ ...input, stream: true, signal: controller.signal })) received.push(chunk);
24
+ assert.deepEqual(received, chunks); assert.equal(calls[1].options.apiKey, key); assert.equal(calls[1].request.signal, controller.signal);
25
+ controller.abort(); await assert.rejects(answer({ ...input, signal: controller.signal })); assert.equal(calls.length, 2);
26
+ });
27
+ test("Perplexity denies before secret lookup and never exposes missing or malformed bindings", async () => {
28
+ let lookups = 0; let clients = 0;
29
+ const options = { configuration: config, authorize: async () => null,
30
+ resolveReference: async () => { lookups++; throw new Error("private-fixture-secret"); }, createClient: () => { clients++; } };
31
+ await assert.rejects(createPerplexityAnswers(options)(input), { code: "connector_access_denied" });
32
+ assert.equal(lookups, 0);
33
+ const allowed = createPerplexityAnswers({ ...options, authorize: async owner => owner });
34
+ await assert.rejects(allowed({ ...input, integrationId: "constructor" }), { code: "connector_not_found" });
35
+ await assert.rejects(allowed({ ...input, question: "" }), { code: "connector_input_invalid" });
36
+ await assert.rejects(allowed(input), error => error.code === "connector_binding_missing" && !String(error).includes("private-fixture-secret"));
37
+ for (const key of ["", "MISSING", "bad\nkey"]) await assert.rejects(createPerplexityAnswers({ ...options, authorize: async owner => owner, resolveReference: async () => key })(input), { code: "connector_binding_missing" });
38
+ assert.equal(clients, 0);
39
+ });
@@ -0,0 +1,140 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { pipedriveProvider } from "../src/server/pipedrive.js";
7
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
8
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
9
+
10
+ async function fixture(t, fetchImpl) {
11
+ const directory = await mkdtemp(join(tmpdir(), "pipedrive-oauth-"));
12
+ t.after(() => rm(directory, { recursive: true, force: true }));
13
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(7) }, activeKeyId: "current" });
14
+ let now = Date.now();
15
+ const callback = "https://app.example/integrations/pipedrive/callback";
16
+ const configuration = { schemaVersion: 1, registrations: { crm: { source: "own", clientId: "client-id",
17
+ tokenEndpointAuthMethod: "client_secret_basic", clientSecretRef: "env:SECRET", callbackUrlRef: "env:CALLBACK" } },
18
+ integrations: { crm: { provider: "pipedrive", accountMode: "per-user", scopes: [],
19
+ authentication: { method: "oauth2", registrationRef: "crm" } } } };
20
+ const service = () => createConnectionService({ configuration, providers: [pipedriveProvider],
21
+ store: createFileConnectionStore({ directory, protection }), authorize: async (owner) => owner,
22
+ now: () => now, resolveReference: async (ref) => ref === "env:SECRET" ? "private-secret" : callback, fetchImpl });
23
+ const input = (subjectId) => ({ context: { applicationId: "app", subjectId }, integrationId: "crm" });
24
+ async function connect(subjectId) {
25
+ const runtime = service();
26
+ const start = await runtime.beginAuthorization(input(subjectId));
27
+ const url = new URL(start.authorizationUrl);
28
+ assert.equal(url.origin + url.pathname, "https://oauth.pipedrive.com/oauth/authorize");
29
+ assert.equal(url.searchParams.get("redirect_uri"), callback);
30
+ assert.equal(url.searchParams.has("code_challenge"), false);
31
+ return { runtime, completion: { ...input(subjectId), callbackUrl: `${callback}?code=${subjectId}&state=${url.searchParams.get("state")}` } };
32
+ }
33
+ return { service, input, connect, callback, advance: () => { now += 61_000; } };
34
+ }
35
+
36
+ test("Pipedrive keeps each company's OAuth route and rotated tokens across restarts", async (t) => {
37
+ const requests = [];
38
+ const rotations = { alice: 0, bob: 0 };
39
+ const f = await fixture(t, async (address, init) => {
40
+ const url = new URL(address);
41
+ const headers = new Headers(init.headers);
42
+ if (url.origin === "https://oauth.pipedrive.com") {
43
+ assert.equal(url.pathname, "/oauth/token");
44
+ assert.match(headers.get("content-type"), /^application\/x-www-form-urlencoded/);
45
+ assert.equal(headers.get("authorization"), `Basic ${Buffer.from("client-id:private-secret").toString("base64")}`);
46
+ const body = new URLSearchParams(init.body);
47
+ const subject = body.get("code") || body.get("refresh_token").split(":")[0];
48
+ assert.equal(body.has("client_secret"), false);
49
+ assert.equal(body.has("code_verifier"), false);
50
+ if (body.get("grant_type") === "authorization_code") assert.equal(body.get("redirect_uri"), f.callback);
51
+ else assert.equal(body.get("refresh_token"), `${subject}:${rotations[subject]}`);
52
+ rotations[subject]++;
53
+ return Response.json({ token_type: "bearer", access_token: `${subject}:${rotations[subject]}`,
54
+ refresh_token: `${subject}:${rotations[subject]}`, expires_in: 60, scope: "base",
55
+ api_domain: `https://${subject}.pipedrive.com` });
56
+ }
57
+ const subject = url.hostname.split(".")[0];
58
+ assert.ok(Object.hasOwn(rotations, subject));
59
+ assert.equal(url.pathname, "/api/v1/users/me");
60
+ assert.equal(headers.get("authorization"), `Bearer ${subject}:${rotations[subject]}`);
61
+ assert.equal(headers.has("x-api-token"), false);
62
+ requests.push(subject);
63
+ return Response.json({ success: true, data: { id: subject === "alice" ? 1 : 2 } });
64
+ });
65
+ for (const subject of ["alice", "bob"]) {
66
+ const { runtime, completion } = await f.connect(subject);
67
+ const result = await runtime.completeAuthorization(completion);
68
+ assert.equal(result.status, "connected");
69
+ assert.equal(result.providerData, undefined);
70
+ await assert.rejects(runtime.completeAuthorization(completion));
71
+ }
72
+ for (let i = 0; i < 2; i++) {
73
+ f.advance();
74
+ for (const subject of ["alice", "bob"]) {
75
+ await f.service().invoke({ ...f.input(subject), operation: "profile.read", input: {} });
76
+ }
77
+ }
78
+ assert.deepEqual(rotations, { alice: 3, bob: 3 });
79
+ assert.deepEqual(requests, ["alice", "bob", "alice", "bob", "alice", "bob"]);
80
+ await assert.rejects(f.service().invoke({ ...f.input("eve"), operation: "profile.read", input: {} }));
81
+ });
82
+
83
+ test("Pipedrive rejects missing or foreign company addresses before sending bearer credentials", async (t) => {
84
+ for (const api_domain of [undefined, "https://evil.example", "https://alice.pipedrive.com.evil.example", "https://alice.pipedrive.com/path", "http://alice.pipedrive.com", "https://user@alice.pipedrive.com", "https://alice.pipedrive.com:443"]) {
85
+ let requests = 0;
86
+ const f = await fixture(t, async (address) => {
87
+ assert.equal(String(address), "https://oauth.pipedrive.com/oauth/token");
88
+ requests++;
89
+ return Response.json({ token_type: "bearer", access_token: "private-token", refresh_token: "refresh", api_domain });
90
+ });
91
+ const { runtime, completion } = await f.connect("alice");
92
+ await assert.rejects(runtime.completeAuthorization(completion), { code: "connector_response_invalid" });
93
+ await assert.rejects(runtime.completeAuthorization(completion));
94
+ await assert.rejects(runtime.invoke({ ...f.input("alice"), operation: "profile.read", input: {} }));
95
+ assert.equal(requests, 1);
96
+ }
97
+ });
98
+
99
+ test("Pipedrive requires reconnection if refresh changes the company destination", async (t) => {
100
+ let exchanges = 0;
101
+ let apiRequests = 0;
102
+ const f = await fixture(t, async (address) => {
103
+ if (String(address) === "https://oauth.pipedrive.com/oauth/token") {
104
+ exchanges++;
105
+ return Response.json({ token_type: "bearer", access_token: "private-token", refresh_token: "refresh", expires_in: 60,
106
+ api_domain: `https://${exchanges === 1 ? "alice" : "other"}.pipedrive.com` });
107
+ }
108
+ apiRequests++;
109
+ return Response.json({ success: true, data: { id: 1 } });
110
+ });
111
+ const { runtime, completion } = await f.connect("alice");
112
+ await runtime.completeAuthorization(completion);
113
+ f.advance();
114
+ await assert.rejects(f.service().invoke({ ...f.input("alice"), operation: "profile.read", input: {} }), { code: "connector_reconnect_required" });
115
+ assert.equal(apiRequests, 1);
116
+ assert.equal((await f.service().status(f.input("alice"))).status, "reconnect-required");
117
+ });
118
+
119
+ test("Pipedrive CRM calls cannot escape the company bound to each OAuth grant", async t => {
120
+ const seen = [];
121
+ const f = await fixture(t, async (address, init) => {
122
+ const url = new URL(address);
123
+ if (url.origin === "https://oauth.pipedrive.com") {
124
+ const subject = new URLSearchParams(init.body).get("code");
125
+ return Response.json({ token_type: "bearer", access_token: subject, refresh_token: subject, expires_in: 3600, api_domain: `https://${subject}.pipedrive.com` });
126
+ }
127
+ const subject = url.hostname.split(".")[0];
128
+ assert.equal(new Headers(init.headers).get("authorization"), `Bearer ${subject}`);
129
+ if (url.pathname === "/api/v1/users/me") return Response.json({ success: true, data: { id: 1 } });
130
+ seen.push({ subject, path: url.pathname, body: init.body });
131
+ return Response.json({ success: true, data: { id: 2, title: "New lead" } });
132
+ });
133
+ for (const subject of ["alice", "bob"]) {
134
+ const { runtime, completion } = await f.connect(subject); await runtime.completeAuthorization(completion);
135
+ await f.service().invoke({ ...f.input(subject), operation: "deals.create", input: { title: "New lead" } });
136
+ }
137
+ assert.deepEqual(seen.map(x => [x.subject, x.path]), [["alice", "/api/v2/deals"], ["bob", "/api/v2/deals"]]);
138
+ await assert.rejects(f.service().invoke({ ...f.input("eve"), operation: "deals.get", input: { id: 2 } }), { code: "connector_reconnect_required" });
139
+ assert.equal(seen.length, 2);
140
+ });
@@ -0,0 +1,200 @@
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 { posthogProvider } from "../src/server/posthog.js";
10
+
11
+ const context = { applicationId: "app-one", subjectId: "analytics-team" };
12
+ const input = { context, integrationId: "analytics" };
13
+ const verificationInput = { distinct_id: "explicit-user" };
14
+ const flags = { flags: { checkout: { key: "checkout", enabled: true, variant: "control",
15
+ reason: { code: "condition_match" }, metadata: { id: 10, version: 1, payload: { colour: "blue" } } } },
16
+ errorsWhileComputingFlags: false, requestId: "fixture-request" };
17
+
18
+ async function fixture(t, region = "eu") {
19
+ const directory = await mkdtemp(path.join(tmpdir(), "posthog-connector-"));
20
+ t.after(() => rm(directory, { recursive: true, force: true }));
21
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(9) }, activeKeyId: "current" });
22
+ const requests = [];
23
+ const state = { token: "phc_fixture_public_token", status: 200, response: flags };
24
+ const options = {
25
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { analytics: {
26
+ provider: "posthog", accountMode: "shared", scopes: [],
27
+ authentication: { method: "api-key", secretRef: "env:POSTHOG_PROJECT_TOKEN" },
28
+ settings: { region, projectId: "12345" }, extensions: { fromCli: true }
29
+ } } },
30
+ providers: [posthogProvider], authorize: async (owner) => owner,
31
+ store: createFileConnectionStore({ directory, protection }),
32
+ resolveReference: async (reference) => { assert.equal(reference, "env:POSTHOG_PROJECT_TOKEN"); return state.token; },
33
+ fetchImpl: async (address, init) => {
34
+ requests.push({ url: new URL(String(address)), init, headers: new Headers(init.headers), body: JSON.parse(init.body) });
35
+ return Response.json(state.response, { status: state.status });
36
+ }
37
+ };
38
+ return { service: createConnectionService(options), options, requests, directory, protection, state };
39
+ }
40
+
41
+ for (const region of ["eu", "us"]) {
42
+ test(`PostHog verifies flags in ${region} and retains file ownership across restart and token rotation`, async (t) => {
43
+ assert.deepEqual(posthogProvider.verificationFields.filter((field) => field.required).map((field) => field.name), Object.keys(verificationInput));
44
+ const { service, options, requests, directory, protection, state } = await fixture(t, region);
45
+ const connected = await service.connectApiKey({ ...input, verificationInput });
46
+ assert.equal(connected.status, "connected");
47
+ const first = requests[0];
48
+ assert.equal(first.url.href, `https://${region}.i.posthog.com/flags?v=2`);
49
+ assert.equal(first.init.method, "POST");
50
+ assert.equal(first.init.redirect, "error");
51
+ assert.equal(first.headers.get("content-type"), "application/json");
52
+ assert.equal(first.headers.has("authorization"), false);
53
+ assert.match(first.headers.get("user-agent"), /^jskit-connectors\/.+posthog-node\//u);
54
+ assert.deepEqual(first.body, { ...verificationInput, api_key: state.token });
55
+ for (const name of await readdir(directory)) {
56
+ const stored = await readFile(path.join(directory, name), "utf8");
57
+ assert.doesNotThrow(() => JSON.parse(stored));
58
+ assert.equal(stored.includes(state.token), false);
59
+ assert.equal(stored.includes(verificationInput.distinct_id), false);
60
+ }
61
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
62
+ assert.deepEqual(await restarted.status(input), connected);
63
+ state.token = "phc_rotated_public_token";
64
+ assert.deepEqual(await restarted.invoke({ ...input, operation: "flags.evaluate", input: verificationInput }), flags);
65
+ assert.equal(requests.at(-1).body.api_key, state.token);
66
+ for (const owner of [{ ...context, applicationId: "other-app" }, { ...context, subjectId: "other-team" }]) {
67
+ await assert.rejects(restarted.invoke({ ...input, context: owner, operation: "flags.evaluate", input: verificationInput }), { code: "connector_reconnect_required" });
68
+ }
69
+ assert.equal(requests.length, 2);
70
+ await restarted.disconnect(input);
71
+ assert.equal((await service.status(input)).status, "disconnected");
72
+ });
73
+ }
74
+
75
+ test("PostHog validates project identity and region using the same portable schema as its form", async (t) => {
76
+ const { options, requests } = await fixture(t);
77
+ const parse = (configuration) => parseIntegrationConfiguration(JSON.stringify(configuration), { providers: [posthogProvider] });
78
+ const configuration = structuredClone(options.configuration);
79
+ delete configuration.integrations.analytics.settings.region;
80
+ assert.deepEqual(parse(configuration), options.configuration);
81
+ for (const projectId of [undefined, "", "abc", "-1", "0", "1.2", "1".repeat(21)]) {
82
+ configuration.integrations.analytics.settings.projectId = projectId;
83
+ assert.throws(() => parse(configuration), (error) => Boolean(error.fieldErrors["integrations.analytics.settings.projectId"]));
84
+ }
85
+ configuration.integrations.analytics.settings = { region: "attacker.invalid", projectId: "12345" };
86
+ assert.throws(() => parse(configuration), (error) => Boolean(error.fieldErrors["integrations.analytics.settings.region"]));
87
+ configuration.integrations.analytics.settings.region = "eu";
88
+ configuration.integrations.analytics.authentication.secretRef = "phc_raw_project_token";
89
+ assert.throws(() => parse(configuration), (error) => Boolean(error.fieldErrors["integrations.analytics.authentication.secretRef"]));
90
+ assert.equal(requests.length, 0);
91
+ });
92
+
93
+ test("PostHog never invents a verification subject or substitutes private analytics credentials", async (t) => {
94
+ const { service, requests, state } = await fixture(t);
95
+ for (const invalid of [undefined, {}, { distinct_id: "" }, { distinct_id: "x".repeat(201) }, { distinct_id: "user", api_key: "phc_other" }]) {
96
+ await assert.rejects(service.connectApiKey({ ...input, verificationInput: invalid }), { code: "connector_input_invalid" });
97
+ }
98
+ for (const token of ["phx_personal_token", "phs_project_secret", "pha_oauth_token", "phr_refresh_token", "phc_", "phc_bad\nheader"]) {
99
+ state.token = token;
100
+ await assert.rejects(service.connectApiKey({ ...input, verificationInput }), { code: "connector_binding_missing" });
101
+ }
102
+ assert.deepEqual(await service.status(input), { status: "unconfigured", configurationError: "connector_binding_missing" });
103
+ state.token = "phc_valid_project_token";
104
+ assert.equal(requests.length, 0);
105
+ assert.equal((await service.status(input)).status, "disconnected");
106
+ });
107
+
108
+ test("PostHog requires re-verification after project or region changes and blocks other API origins", async (t) => {
109
+ const { service, options, requests } = await fixture(t);
110
+ await service.connectApiKey({ ...input, verificationInput });
111
+ for (const [field, value] of [["projectId", "999"], ["region", "us"]]) {
112
+ const configuration = structuredClone(options.configuration);
113
+ configuration.integrations.analytics.settings[field] = value;
114
+ const changed = createConnectionService({ ...options, configuration });
115
+ assert.equal((await changed.status(input)).status, "reconnect-required");
116
+ await assert.rejects(changed.invoke({ ...input, operation: "flags.evaluate", input: verificationInput }), { code: "connector_reconnect_required" });
117
+ }
118
+ for (const origin of ["https://us.i.posthog.com", "https://eu.posthog.com", "https://attacker.invalid"]) {
119
+ const wrongHost = createConnectionService({ ...options, providers: [{ ...posthogProvider, operations: {
120
+ "flags.evaluate": { scopes: [], request: () => ({ method: "POST", url: `${origin}/flags?v=2`, body: verificationInput }) }
121
+ } }] });
122
+ await assert.rejects(wrongHost.invoke({ ...input, operation: "flags.evaluate", input: verificationInput }), { code: "connector_destination_invalid" });
123
+ }
124
+ assert.equal(requests.length, 1);
125
+ });
126
+
127
+ test("PostHog preserves flag metadata and subject properties without accepting credential or URL overrides", async (t) => {
128
+ const { service, requests, state } = await fixture(t);
129
+ await service.connectApiKey({ ...input, verificationInput });
130
+ const values = { distinct_id: "user+&=42", groups: { company: "company-42" }, person_properties: { plan: "paid", enabled: false } };
131
+ assert.deepEqual(await service.invoke({ ...input, operation: "flags.evaluate", input: values }), flags);
132
+ assert.deepEqual(requests.at(-1).body, { ...values, api_key: state.token });
133
+ assert.equal(requests.at(-1).url.search, "?v=2");
134
+ assert.equal(Object.hasOwn(values, "api_key"), false);
135
+ for (const invalid of [{ ...values, api_key: "phc_other" }, { ...values, url: "https://attacker.invalid" }, { ...values, groups: [] }, { ...values, person_properties: "plan=paid" }]) {
136
+ await assert.rejects(service.invoke({ ...input, operation: "flags.evaluate", input: invalid }), { code: "connector_input_invalid" });
137
+ }
138
+ assert.equal(requests.length, 2);
139
+ state.response = { flags: {}, errorsWhileComputingFlags: false };
140
+ assert.deepEqual(await service.invoke({ ...input, operation: "flags.evaluate", input: values }), state.response);
141
+ });
142
+
143
+ test("PostHog HTTP-200 flag quota and partial failures never imply a usable verified connection", async (t) => {
144
+ const { service, state } = await fixture(t);
145
+ for (const [response, code] of [
146
+ [{ flags: {}, errorsWhileComputingFlags: false, quotaLimited: ["feature_flags"] }, "connector_quota_limited"],
147
+ [{ ...flags, errorsWhileComputingFlags: true }, "connector_provider_failed"],
148
+ [{ flags: {} }, "connector_response_invalid"],
149
+ [{ ...flags, flags: [] }, "connector_response_invalid"],
150
+ [{ ...flags, quotaLimited: "feature_flags" }, "connector_response_invalid"],
151
+ [{ ...flags, flags: { checkout: { enabled: "yes" } } }, "connector_response_invalid"],
152
+ [{ ...flags, flags: { checkout: { enabled: true, variant: {} } } }, "connector_response_invalid"]
153
+ ]) {
154
+ state.response = response;
155
+ await assert.rejects(service.connectApiKey({ ...input, verificationInput }), { code });
156
+ assert.equal((await service.status(input)).status, "disconnected");
157
+ }
158
+ });
159
+
160
+ test("PostHog capture is an explicit separate operation and preserves acceptance and quota information", async (t) => {
161
+ const { service, requests, state } = await fixture(t);
162
+ await service.connectApiKey({ ...input, verificationInput });
163
+ state.response = { status: "Ok", quota_limited: ["events"] };
164
+ const values = { event: "checkout_completed", distinct_id: "user-one", properties: { value: 19, currency: "EUR" } };
165
+ assert.deepEqual(await service.invoke({ ...input, operation: "events.capture", input: values }), state.response);
166
+ assert.equal(requests.at(-1).url.href, "https://eu.i.posthog.com/i/v0/e/");
167
+ assert.deepEqual(requests.at(-1).body, { ...values, api_key: state.token });
168
+ for (const invalid of [{ ...values, event: "" }, { event: "checkout" }, { ...values, api_key: "phc_other" }, { ...values, properties: { token: "phc_other" } }, { ...values, properties: { distinct_id: "other-user" } }]) {
169
+ await assert.rejects(service.invoke({ ...input, operation: "events.capture", input: invalid }), { code: "connector_input_invalid" });
170
+ }
171
+ assert.equal(requests.length, 2);
172
+ state.response = { status: "Ok" };
173
+ await service.invoke({ ...input, operation: "events.capture", input: { event: "page_opened", distinct_id: "user-one" } });
174
+ assert.deepEqual(requests.at(-1).body.properties, {});
175
+ for (const malformed of [{}, { status: "Error" }, { status: "Ok", quota_limited: "events" }]) {
176
+ state.response = malformed;
177
+ await assert.rejects(service.invoke({ ...input, operation: "events.capture", input: values }), { code: "connector_response_invalid" });
178
+ }
179
+ });
180
+
181
+ test("PostHog rejects HTTP failures without leaking response bodies and marks a rejected token for reconnection", async (t) => {
182
+ const { service, state } = await fixture(t);
183
+ for (const [status, code] of [[401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
184
+ state.status = status;
185
+ state.response = { detail: state.token };
186
+ await assert.rejects(service.connectApiKey({ ...input, verificationInput }), (error) => {
187
+ assert.equal(error.code, code);
188
+ assert.equal(error.message.includes(state.token), false);
189
+ assert.equal(JSON.stringify(error).includes(state.token), false);
190
+ return true;
191
+ });
192
+ assert.equal((await service.status(input)).status, "disconnected");
193
+ }
194
+ state.status = 200;
195
+ state.response = flags;
196
+ await service.connectApiKey({ ...input, verificationInput });
197
+ state.status = 401;
198
+ await assert.rejects(service.invoke({ ...input, operation: "flags.evaluate", input: verificationInput }), { code: "connector_reconnect_required" });
199
+ assert.equal((await service.status(input)).status, "reconnect-required");
200
+ });